tcp-client 0.8.0 → 0.9.3

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: 14f787ad4e8e06910bfebf67755ae7142183df4c4fb090edce84aae7d497a2b6
4
- data.tar.gz: 6349bea2eac6bac053c724e0c6a162b1fe99502e94cf1c6734854d97f194f37a
3
+ metadata.gz: 75811fed2b0735c2cac6a4e9671e138df88e6b8ada2be03a0d86b4b83b37b863
4
+ data.tar.gz: 9c715081912711178a0038d9af9832cf6c45e2620c2cff6dad899565de86be02
5
5
  SHA512:
6
- metadata.gz: b53fafbf091a67a329832663c058b4e8243b36a779f8bc52cc7f37de556ad0aa016245e0a059489b972e6afaccfea7d26a63a735686d5f50d88036a2f4ecaca1
7
- data.tar.gz: 4a6ff368e221073c5addfab51e31df1b094ee3f69e09ebe3f38d6d552ebc761b14c552a1a2cd90dcfd148b1ba34ebb1b2e3abfcea9622ded8413870f21a60285
6
+ metadata.gz: d2842f5bd4fa2806c15bb94d6c1da9a897be40959a5db9033f404f1c980648a997daee0d49ad1fb75e4cf680644f72dfb04e880eb0c9e9ba9207d6ee3dd75240
7
+ data.tar.gz: 174878bab1414b1ef49bd67a1f296d21e463bd7d817442340192aace8af88e56d941b647ad43ff0ed5126e2c2ab16e0159c0467dd7a08289d93c423ca520e2cb
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
- local/
2
1
  tmp/
3
2
  pkg/
3
+ local/
4
+ doc/
5
+ .yardoc/
4
6
  gems.locked
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --readme README.md
2
+ --title 'tcp-client Documentation'
3
+ --charset utf-8
4
+ --markup markdown
5
+ 'lib/**/*.rb' - 'LICENSE'
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A TCP client implementation with working timeout support.
4
4
 
5
+ - Gem: [rubygems.org](https://rubygems.org/gems/tcp-client)
6
+ - Source: [github.com](https://github.com/mblumtritt/tcp-client)
7
+ - Help: [rubydoc.info](https://rubydoc.info/github/mblumtritt/tcp-client/main/index)
8
+
5
9
  ## Description
6
10
 
7
11
  This Gem implements a TCP client with (optional) SSL support. It is an easy to use, versatile configurable client that can correctly handle time limits. Unlike other implementations, this client respects predefined/configurable time limits for each method (`connect`, `read`, `write`). Deadlines for a sequence of read/write actions can also be monitored.
@@ -30,7 +34,9 @@ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
30
34
  end
31
35
  ```
32
36
 
33
- ### Installation
37
+ For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
38
+
39
+ ## Installation
34
40
 
35
41
  Use [Bundler](http://gembundler.com/) to use TCPClient in your own project:
36
42
 
data/gems.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source('https://rubygems.org') { gemspec }
3
+ source 'https://rubygems.org'
4
+ gemspec
@@ -3,9 +3,51 @@
3
3
  require 'socket'
4
4
 
5
5
  class TCPClient
6
+ #
7
+ # The address used by a TCPClient
8
+ #
6
9
  class Address
7
- attr_reader :hostname, :addrinfo
10
+ #
11
+ # @return [String] the host name
12
+ #
13
+ attr_reader :hostname
8
14
 
15
+ #
16
+ # @return [Addrinfo] the address info
17
+ #
18
+ attr_reader :addrinfo
19
+
20
+ #
21
+ # Initializes an address
22
+ # @overload initialize(addr)
23
+ # The addr can be specified as
24
+ #
25
+ # - a valid named address containing the port like "my.host.test:80"
26
+ # - a valid TCPv4 address like "142.250.181.206:80"
27
+ # - a valid TCPv6 address like
28
+ # "[2001:16b8:5093:3500:ad77:abe6:eb88:47b6]:80"
29
+ #
30
+ # @example create an Address instance with a host name and port
31
+ # Address.new('www.google.com:80')
32
+ #
33
+ # @param addr [String] address containing host and port name
34
+ #
35
+ #
36
+ # @overload initialize(addrinfo)
37
+ #
38
+ # @example create an Address with an Addrinfo
39
+ # Address.new(Addrinfo.tcp('www.google.com', 'http'))
40
+ #
41
+ # @param addrinfo [Addrinfo] containing the addressed host and port
42
+ #
43
+ # @overload initialize(port)
44
+ # Adresses the port on the local machine.
45
+ #
46
+ # @example create an Address for localhost on port 80
47
+ # Address.new(80)
48
+ #
49
+ # @param port [Integer] the addressed port
50
+ #
9
51
  def initialize(addr)
10
52
  case addr
11
53
  when self.class
@@ -20,24 +62,28 @@ class TCPClient
20
62
  @addrinfo.freeze
21
63
  end
22
64
 
65
+ #
66
+ # @return [String] text representation of self as "host:port"
67
+ #
23
68
  def to_s
24
69
  return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
25
70
  "#{@hostname}:#{@addrinfo.ip_port}"
26
71
  end
27
72
 
28
- def to_hash
73
+ #
74
+ # @return [Hash] containing the host and port
75
+ #
76
+ def to_h
29
77
  { host: @hostname, port: @addrinfo.ip_port }
30
78
  end
31
79
 
32
- def to_h(*args)
33
- args.empty? ? to_hash : to_hash.slice(*args)
34
- end
35
-
80
+ # @!visibility private
36
81
  def ==(other)
37
- to_hash == other.to_hash
82
+ to_h == other.to_h
38
83
  end
39
84
  alias eql? ==
40
85
 
86
+ # @!visibility private
41
87
  def equal?(other)
42
88
  self.class == other.class && self == other
43
89
  end
@@ -3,25 +3,63 @@
3
3
  require_relative 'errors'
4
4
 
5
5
  class TCPClient
6
+ #
7
+ # A Configuration is used to configure the behavior of a {TCPClient} instance.
8
+ #
9
+ # It allows to specify to monitor timeout, how to handle exceptions, if SSL
10
+ # should be used and to setup the underlying Socket.
11
+ #
6
12
  class Configuration
13
+ #
14
+ # @overload create(options)
15
+ # Shorthand to create a new configuration with given options.
16
+ #
17
+ # @example
18
+ # config = TCPClient::Configuration.create(buffered: false)
19
+ #
20
+ # @param options [Hash] see {#initialize} for details
21
+ #
22
+ # @return [Configuration] the initialized configuration
23
+ #
24
+ # @overload create(&block)
25
+ # Shorthand to create a new configuration within a code block.
26
+ #
27
+ # @example
28
+ # config = TCPClient::Configuration.create do |cfg|
29
+ # cfg.buffered = false
30
+ # cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
31
+ # end
32
+ #
33
+ # @yieldparam configuration {Configuration}
34
+ #
35
+ # @return [Configuration] the initialized configuration
36
+ #
7
37
  def self.create(options = {})
8
- ret = new(options)
9
- yield(ret) if block_given?
10
- ret
11
- end
12
-
13
- attr_reader :buffered,
14
- :keep_alive,
15
- :reverse_lookup,
16
- :normalize_network_errors,
17
- :connect_timeout,
18
- :read_timeout,
19
- :write_timeout,
20
- :connect_timeout_error,
21
- :read_timeout_error,
22
- :write_timeout_error
23
- attr_accessor :ssl_params
38
+ configuration = new(options)
39
+ yield(configuration) if block_given?
40
+ configuration
41
+ end
24
42
 
43
+ #
44
+ # Intializes the instance with given options.
45
+ #
46
+ # @param options [Hash]
47
+ # @option options [Boolean] :buffered, see {#buffered}
48
+ # @option options [Boolean] :keep_alive, see {#keep_alive}
49
+ # @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
50
+ # @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
51
+ # @option options [Numeric] :connect_timeout, see {#connect_timeout}
52
+ # @option options [Exception] :connect_timeout_error, see
53
+ # {#connect_timeout_error}
54
+ # @option options [Numeric] :read_timeout, see {#read_timeout}
55
+ # @option options [Exception] :read_timeout_error, see {#read_timeout_error}
56
+ # @option options [Numeric] :write_timeout, see {#write_timeout}
57
+ # @option options [Exception] :write_timeout_error, see
58
+ # {#write_timeout_error}
59
+ # @option options [Boolean] :normalize_network_errors, see
60
+ # {#normalize_network_errors}
61
+ #
62
+ #
25
63
  def initialize(options = {})
26
64
  @buffered = @keep_alive = @reverse_lookup = true
27
65
  self.timeout = @ssl_params = nil
@@ -32,107 +70,259 @@ class TCPClient
32
70
  options.each_pair { |attribute, value| set(attribute, value) }
33
71
  end
34
72
 
35
- def freeze
36
- @ssl_params.freeze
37
- super
73
+ # @!group Instance Attributes Socket Level
74
+
75
+ #
76
+ # Enables/disables use of Socket-level buffering
77
+ #
78
+ # @return [true] if the connection is allowed to use internal buffers
79
+ # (default)
80
+ # @return [false] if buffering is not allowed
81
+ #
82
+ attr_reader :buffered
83
+
84
+ def buffered=(value)
85
+ @buffered = value ? true : false
38
86
  end
39
87
 
40
- def initialize_copy(_org)
41
- super
42
- @ssl_params = Hash[@ssl_params] if @ssl_params
43
- self
88
+ #
89
+ # Enables/disables use of Socket-level keep alive handling.
90
+ #
91
+ # @return [true] if the connection is allowed to use keep alive signals
92
+ # (default)
93
+ # @return [false] if the connection should not check keep alive
94
+ #
95
+ attr_reader :keep_alive
96
+
97
+ def keep_alive=(value)
98
+ @keep_alive = value ? true : false
99
+ end
100
+
101
+ #
102
+ # Enables/disables address lookup.
103
+ #
104
+ # @return [true] if the connection is allowed to lookup the address
105
+ # (default)
106
+ # @return [false] if the address lookup is not required
107
+ #
108
+ attr_reader :reverse_lookup
109
+
110
+ def reverse_lookup=(value)
111
+ @reverse_lookup = value ? true : false
44
112
  end
45
113
 
114
+ #
115
+ # @!parse attr_reader :ssl?
116
+ # @return [Boolean] wheter SSL is configured, see {#ssl_params}
117
+ #
46
118
  def ssl?
47
119
  @ssl_params ? true : false
48
120
  end
49
121
 
50
- def ssl=(value)
122
+ #
123
+ # Parameters used to initialize a SSL context.
124
+ #
125
+ # @return [Hash<Symbol, Object>] SSL parameters for the SSL context
126
+ # @return [nil] if no SSL should be used (default)
127
+ #
128
+ attr_reader :ssl_params
129
+
130
+ def ssl_params=(value)
51
131
  @ssl_params =
52
132
  if value.respond_to?(:to_hash)
53
133
  Hash[value.to_hash]
134
+ elsif value.respond_to?(:to_h)
135
+ value.nil? ? nil : Hash[value.to_h]
54
136
  else
55
137
  value ? {} : nil
56
138
  end
57
139
  end
140
+ alias ssl= ssl_params=
58
141
 
59
- def buffered=(value)
60
- @buffered = value ? true : false
61
- end
142
+ # @!endgroup
62
143
 
63
- def keep_alive=(value)
64
- @keep_alive = value ? true : false
144
+ # @!group Instance Attributes Timeout Monitoring
145
+
146
+ #
147
+ # The maximum time in seconds to establish a connection.
148
+ #
149
+ # @return [Numeric] maximum time in seconds
150
+ # @return [nil] if the connect time should not be monitored (default)
151
+ #
152
+ # @see TCPClient#connect
153
+ #
154
+ attr_reader :connect_timeout
155
+
156
+ def connect_timeout=(value)
157
+ @connect_timeout = seconds(value)
65
158
  end
66
159
 
67
- def reverse_lookup=(value)
68
- @reverse_lookup = value ? true : false
160
+ #
161
+ # The exception class which will be raised if {TCPClient#connect} can not
162
+ # be finished in time.
163
+ #
164
+ # @return [Class] exception class raised
165
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
166
+ #
167
+ attr_reader :connect_timeout_error
168
+
169
+ def connect_timeout_error=(value)
170
+ raise(NotAnExceptionError, value) unless exception_class?(value)
171
+ @connect_timeout_error = value
69
172
  end
70
173
 
71
- def normalize_network_errors=(value)
72
- @normalize_network_errors = value ? true : false
174
+ #
175
+ # The maximum time in seconds to read from a connection.
176
+ #
177
+ # @return [Numeric] maximum time in seconds
178
+ # @return [nil] if the read time should not be monitored (default)
179
+ #
180
+ # @see TCPClient#read
181
+ #
182
+ attr_reader :read_timeout
183
+
184
+ def read_timeout=(value)
185
+ @read_timeout = seconds(value)
73
186
  end
74
187
 
75
- def timeout=(seconds)
76
- @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
188
+ #
189
+ # The exception class which will be raised if {TCPClient#read} can not be
190
+ # finished in time.
191
+ #
192
+ # @return [Class] exception class raised
193
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
194
+ #
195
+ attr_reader :read_timeout_error
196
+
197
+ def read_timeout_error=(value)
198
+ raise(NotAnExceptionError, value) unless exception_class?(value)
199
+ @read_timeout_error = value
77
200
  end
78
201
 
79
- def connect_timeout=(seconds)
80
- @connect_timeout = seconds(seconds)
202
+ #
203
+ # The maximum time in seconds to write to a connection.
204
+ #
205
+ # @return [Numeric] maximum time in seconds
206
+ # @return [nil] if the write time should not be monitored (default)
207
+ #
208
+ # @see TCPClient#write
209
+ #
210
+ attr_reader :write_timeout
211
+
212
+ def write_timeout=(value)
213
+ @write_timeout = seconds(value)
81
214
  end
82
215
 
83
- def read_timeout=(seconds)
84
- @read_timeout = seconds(seconds)
216
+ #
217
+ # The exception class which will be raised if {TCPClient#write} can not be
218
+ # finished in time.
219
+ #
220
+ # @return [Class] exception class raised
221
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
222
+ #
223
+ attr_reader :write_timeout_error
224
+
225
+ def write_timeout_error=(value)
226
+ raise(NotAnExceptionError, value) unless exception_class?(value)
227
+ @write_timeout_error = value
85
228
  end
86
229
 
87
- def write_timeout=(seconds)
88
- @write_timeout = seconds(seconds)
230
+ #
231
+ # @attribute [w] timeout
232
+ # Shorthand to set maximum time in seconds for all timeout monitoring.
233
+ #
234
+ # @return [Numeric] maximum time in seconds for any actwion
235
+ # @return [nil] if all timeout monitoring should be disabled (default)
236
+ #
237
+ # @see #connect_timeout
238
+ # @see #read_timeout
239
+ # @see #write_timeout
240
+ #
241
+ def timeout=(value)
242
+ @connect_timeout = @write_timeout = @read_timeout = seconds(value)
89
243
  end
90
244
 
91
- def timeout_error=(exception)
92
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
245
+ #
246
+ # @attribute [w] timeout_error
247
+ # Shorthand to set the exception class wich will by raised by any timeout.
248
+ #
249
+ # @return [Class] exception class raised
250
+ #
251
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
252
+ #
253
+ # @see #connect_timeout_error
254
+ # @see #read_timeout_error
255
+ # @see #write_timeout_error
256
+ #
257
+ def timeout_error=(value)
258
+ raise(NotAnExceptionError, value) unless exception_class?(value)
93
259
  @connect_timeout_error =
94
- @read_timeout_error = @write_timeout_error = exception
260
+ @read_timeout_error = @write_timeout_error = value
95
261
  end
96
262
 
97
- def connect_timeout_error=(exception)
98
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
99
- @connect_timeout_error = exception
100
- end
263
+ # @!endgroup
101
264
 
102
- def read_timeout_error=(exception)
103
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
104
- @read_timeout_error = exception
105
- end
265
+ #
266
+ # Enables/disables if network exceptions should be raised as {NetworkError}.
267
+ #
268
+ # This allows to handle all network/socket related exceptions like
269
+ # `SocketError`, `OpenSSL::SSL::SSLError`, `IOError`, etc. in a uniform
270
+ # manner. If this option is set to true all these error cases are raised as
271
+ # {NetworkError} and can be easily captured.
272
+ #
273
+ # @return [true] if all network exceptions should be raised as
274
+ # {NetworkError}
275
+ # @return [false] if socket/system errors should not be normalzed (default)
276
+ #
277
+ attr_reader :normalize_network_errors
106
278
 
107
- def write_timeout_error=(exception)
108
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
109
- @write_timeout_error = exception
279
+ def normalize_network_errors=(value)
280
+ @normalize_network_errors = value ? true : false
110
281
  end
111
282
 
112
- def to_hash
283
+ #
284
+ # Convert `self` to a Hash containing all attributes.
285
+ #
286
+ # @return [Hash<Symbol, Object>]
287
+ #
288
+ # @see #initialize
289
+ #
290
+ def to_h
113
291
  {
114
292
  buffered: @buffered,
115
293
  keep_alive: @keep_alive,
116
294
  reverse_lookup: @reverse_lookup,
295
+ ssl_params: @ssl_params,
117
296
  connect_timeout: @connect_timeout,
118
297
  connect_timeout_error: @connect_timeout_error,
119
298
  read_timeout: @read_timeout,
120
299
  read_timeout_error: @read_timeout_error,
121
300
  write_timeout: @write_timeout,
122
301
  write_timeout_error: @write_timeout_error,
123
- ssl_params: @ssl_params
302
+ normalize_network_errors: @normalize_network_errors
124
303
  }
125
304
  end
126
305
 
127
- def to_h(*args)
128
- args.empty? ? to_hash : to_hash.slice(*args)
306
+ # @!visibility private
307
+ def freeze
308
+ @ssl_params.freeze
309
+ super
310
+ end
311
+
312
+ # @!visibility private
313
+ def initialize_copy(_org)
314
+ super
315
+ @ssl_params = Hash[@ssl_params] if @ssl_params
316
+ self
129
317
  end
130
318
 
319
+ # @!visibility private
131
320
  def ==(other)
132
- to_hash == other.to_hash
321
+ to_h == other.to_h
133
322
  end
134
323
  alias eql? ==
135
324
 
325
+ # @!visibility private
136
326
  def equal?(other)
137
327
  self.class == other.class && self == other
138
328
  end
@@ -6,16 +6,50 @@ class TCPClient
6
6
  @default_configuration = Configuration.new
7
7
 
8
8
  class << self
9
+ #
10
+ # The default configuration.
11
+ # This is used by default if no dedicated configuration was specified to
12
+ # {.open} or {#connect}.
13
+ #
14
+ # @return [Configuration]
15
+ #
9
16
  attr_reader :default_configuration
10
17
 
18
+ #
19
+ # Configure the {.default_configuration} which is used if no dedicated
20
+ # configuration was specified to {.open} or {#connect}.
21
+ #
22
+ # @example
23
+ # TCPClient.configure do |cfg|
24
+ # cfg.buffered = false
25
+ # cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
26
+ # end
27
+ #
28
+ # @param options [Hash] see {Configuration#initialize} for details
29
+ #
30
+ # @yieldparam cfg {Configuration} the new configuration
31
+ #
32
+ # @return [Configuration] the new default configuration
33
+ #
11
34
  def configure(options = {}, &block)
12
35
  @default_configuration = Configuration.create(options, &block)
13
36
  end
14
37
  end
15
38
 
16
39
  class Configuration
17
- def self.default
18
- TCPClient.default_configuration
40
+ class << self
41
+ #
42
+ # @!parse attr_reader :default
43
+ # @return [Configuration] used by default if no dedicated configuration
44
+ # was specified
45
+ #
46
+ # @see TCPClient.open
47
+ # @see TCPClient.with_deadline
48
+ # @see TCPClient#connect
49
+ #
50
+ def default
51
+ TCPClient.default_configuration
52
+ end
19
53
  end
20
54
  end
21
55
  end
@@ -1,78 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
+ #
5
+ # Raised when a SSL connection should be establshed but the OpenSSL gem is not available.
6
+ #
4
7
  class NoOpenSSLError < RuntimeError
5
8
  def initialize
6
9
  super('OpenSSL is not available')
7
10
  end
8
11
  end
9
12
 
13
+ #
14
+ # Raised when a method requires a callback block but no such block is specified.
15
+ #
10
16
  class NoBlockGivenError < ArgumentError
11
17
  def initialize
12
18
  super('no block given')
13
19
  end
14
20
  end
15
21
 
22
+ #
23
+ # Raised when an invalid timeout value was specified.
24
+ #
16
25
  class InvalidDeadLineError < ArgumentError
26
+ #
27
+ # @param timeout [Object] the invalid value
28
+ #
17
29
  def initialize(timeout)
18
30
  super("invalid deadline - #{timeout}")
19
31
  end
20
32
  end
21
33
 
34
+ #
35
+ # Raised by {Configuration} when an undefined attribute should be set.
36
+ #
22
37
  class UnknownAttributeError < ArgumentError
38
+ #
39
+ # @param attribute [Object] the undefined atttribute
40
+ #
23
41
  def initialize(attribute)
24
42
  super("unknown attribute - #{attribute}")
25
43
  end
26
44
  end
27
45
 
46
+ #
47
+ # Raised when a given timeout exception parameter is not an exception class.
48
+ #
28
49
  class NotAnExceptionError < TypeError
50
+ #
51
+ # @param object [Object] the invalid object
52
+ #
29
53
  def initialize(object)
30
54
  super("exception class required - #{object.inspect}")
31
55
  end
32
56
  end
33
57
 
34
- NetworkError = Class.new(StandardError)
58
+ #
59
+ # Base exception class for all network related errors.
60
+ #
61
+ # Will be raised for any system level network error when {Configuration.normalize_network_errors} is configured.
62
+ #
63
+ # You should catch this exception class when you like to handle any relevant {TCPClient} error.
64
+ #
65
+ class NetworkError < StandardError
66
+ end
35
67
 
68
+ #
69
+ # Raised when a {TCPClient} instance should read/write from/to the network but is not connected.
70
+ #
36
71
  class NotConnectedError < NetworkError
37
72
  def initialize
38
73
  super('client not connected')
39
74
  end
40
75
  end
41
76
 
77
+ #
78
+ # Base exception class for a detected timeout.
79
+ #
80
+ # You should catch this exception class when you like to handle any timeout error.
81
+ #
42
82
  class TimeoutError < NetworkError
83
+ #
84
+ # Initializes the instance with an optional message.
85
+ #
86
+ # The message will be generated from {#action} when not specified.
87
+ #
88
+ # @overload initialize
89
+ # @overload initialize(message)
90
+ #
91
+ # @param message [#to_s] the error message
92
+ #
43
93
  def initialize(message = nil)
44
94
  super(message || "unable to #{action} in time")
45
95
  end
46
96
 
97
+ #
98
+ # @attribute [r] action
99
+ # @return [Symbol] the action which timed out
100
+ #
47
101
  def action
48
102
  :process
49
103
  end
50
104
  end
51
105
 
106
+ #
107
+ # Raised by default whenever a {TCPClient.connect} timed out.
108
+ #
52
109
  class ConnectTimeoutError < TimeoutError
110
+ #
111
+ # @attribute [r] action
112
+ # @return [Symbol] the action which timed out: `:connect`
113
+ #
53
114
  def action
54
115
  :connect
55
116
  end
56
117
  end
57
118
 
119
+ #
120
+ # Raised by default whenever a {TCPClient#read} timed out.
121
+ #
58
122
  class ReadTimeoutError < TimeoutError
123
+ #
124
+ # @attribute [r] action
125
+ # @return [Symbol] the action which timed out: :read`
126
+ #
59
127
  def action
60
128
  :read
61
129
  end
62
130
  end
63
131
 
132
+ #
133
+ # Raised by default whenever a {TCPClient#write} timed out.
134
+ #
64
135
  class WriteTimeoutError < TimeoutError
136
+ #
137
+ # @attribute [r] action
138
+ # @return [Symbol] the action which timed out: `:write`
139
+ #
65
140
  def action
66
141
  :write
67
142
  end
68
143
  end
69
144
 
70
- NoOpenSSL = NoOpenSSLError
71
- NoBlockGiven = NoBlockGivenError
72
- InvalidDeadLine = InvalidDeadLineError
73
- UnknownAttribute = UnknownAttributeError
74
- NotAnException = NotAnExceptionError
75
- NotConnected = NotConnectedError
145
+ NoOpenSSL = NoOpenSSLError # @!visibility private
146
+ NoBlockGiven = NoBlockGivenError # @!visibility private
147
+ InvalidDeadLine = InvalidDeadLineError # @!visibility private
148
+ UnknownAttribute = UnknownAttributeError # @!visibility private
149
+ NotAnException = NotAnExceptionError # @!visibility private
150
+ NotConnected = NotConnectedError # @!visibility private
76
151
  deprecate_constant(
77
152
  :NoOpenSSL,
78
153
  :NoBlockGiven,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module IOWithDeadlineMixin
3
+ # @!visibility private
4
+ module IOWithDeadlineMixin # :nodoc:
4
5
  def self.included(mod)
5
6
  methods = mod.instance_methods
6
7
  if methods.index(:wait_writable) && methods.index(:wait_readable)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.3'
5
5
  end
data/lib/tcp-client.rb CHANGED
@@ -9,7 +9,47 @@ require_relative 'tcp-client/configuration'
9
9
  require_relative 'tcp-client/default_configuration'
10
10
  require_relative 'tcp-client/version'
11
11
 
12
+ #
13
+ # Client class to communicate with a server via TCP w/o SSL.
14
+ #
15
+ # All connect/read/write actions can be monitored to ensure that all actions
16
+ # terminate before given time limits - or raise an exception.
17
+ #
18
+ # @example request to Google.com and limit network interactions to 1.5 seconds
19
+ # TCPClient.with_deadline(1.5, 'www.google.com:443') do |client|
20
+ # client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
21
+ # client.read(12)
22
+ # end
23
+ # # => "HTTP/1.1 200"
24
+ #
25
+ #
12
26
  class TCPClient
27
+ #
28
+ # Creates a new instance which is connected to the server on the given
29
+ # `address`.
30
+ #
31
+ # If no `configuration` is given, the {.default_configuration} will be used.
32
+ #
33
+ # If an optional block is given, then the block's result is returned and the
34
+ # connection will be closed when the block execution ends.
35
+ # This can be used to create an ad-hoc connection which is garanteed to be
36
+ # closed.
37
+ #
38
+ # If no block is giiven the connected client instance is returned.
39
+ # This can be used as a shorthand to create & connect a client.
40
+ #
41
+ # @param address [Address, String, Addrinfo, Integer] the address to connect
42
+ # to, see {Address#initialize} for valid formats
43
+ # @param configuration [Configuration] the {Configuration} to be used for
44
+ # this instance
45
+ #
46
+ # @yieldparam client [TCPClient] the connected client
47
+ # @yieldreturn [Object] any result
48
+ #
49
+ # @return [Object, TCPClient] the block result or the connected client
50
+ #
51
+ # @see #connect
52
+ #
13
53
  def self.open(address, configuration = nil)
14
54
  client = new
15
55
  client.connect(Address.new(address), configuration)
@@ -18,6 +58,34 @@ class TCPClient
18
58
  client.close if block_given?
19
59
  end
20
60
 
61
+ #
62
+ # Yields a new instance which is connected to the server on the given
63
+ # `address`.It limits all {#read} and {#write} actions within the block to
64
+ # the given time.
65
+ #
66
+ # It ensures to close the connection when the block execution ends and returns
67
+ # the block`s result.
68
+ #
69
+ # This can be used to create an ad-hoc connection which is garanteed to be
70
+ # closed and which {#read}/{#write} call sequence should not last longer than
71
+ # the `timeout`.
72
+ #
73
+ # If no `configuration` is given, the {.default_configuration} will be used.
74
+ #
75
+ # @param timeout [Numeric] maximum time in seconds for all {#read} and
76
+ # {#write} calls within the block
77
+ # @param address [Address, String, Addrinfo, Integer] the address to connect
78
+ # to, see {Address#initialize} for valid formats
79
+ # @param configuration [Configuration] the {Configuration} to be used for
80
+ # this instance
81
+ #
82
+ # @yieldparam client [TCPClient] the connected client
83
+ # @yieldreturn [Object] any result
84
+ #
85
+ # @return [Object] the block's result
86
+ #
87
+ # @see #with_deadline
88
+ #
21
89
  def self.with_deadline(timeout, address, configuration = nil)
22
90
  client = nil
23
91
  raise(NoBlockGivenError) unless block_given?
@@ -30,21 +98,29 @@ class TCPClient
30
98
  client&.close
31
99
  end
32
100
 
33
- attr_reader :address, :configuration
101
+ #
102
+ # @return [Address] the address used by this client instance
103
+ #
104
+ attr_reader :address
34
105
 
35
- def to_s
36
- @address&.to_s || ''
37
- end
106
+ #
107
+ # @return [Configuration] the configuration used by this client instance
108
+ #
109
+ attr_reader :configuration
38
110
 
39
- def connect(address, configuration = nil, timeout: nil, exception: nil)
40
- close if @socket
41
- raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
42
- @address = Address.new(address)
43
- @configuration = (configuration || Configuration.default).dup
44
- @socket = create_socket(timeout, exception)
45
- self
111
+ #
112
+ # @!parse attr_reader :closed?
113
+ # @return [Boolean] true when the connection is closed, false when connected
114
+ #
115
+ def closed?
116
+ @socket.nil? || @socket.closed?
46
117
  end
47
118
 
119
+ #
120
+ # Close the current connection if connected.
121
+ #
122
+ # @return [self]
123
+ #
48
124
  def close
49
125
  @socket&.close
50
126
  self
@@ -54,20 +130,67 @@ class TCPClient
54
130
  @socket = @deadline = nil
55
131
  end
56
132
 
57
- def closed?
58
- @socket.nil? || @socket.closed?
133
+ #
134
+ # Establishes a new connection to a given `address`.
135
+ #
136
+ # It accepts a connection-specific configuration or uses the
137
+ # {.default_configuration}. The {#configuration} used by this instance will
138
+ # be a copy of the configuration used for this method call. This allows to
139
+ # configure the behavior per connection.
140
+ #
141
+ # The optional `timeout` and `exception` parameters allow to override the
142
+ # `connect_timeout` and `connect_timeout_error` values.
143
+ #
144
+ # @param address [Address, String, Addrinfo, Integer] the address to connect
145
+ # to, see {Address#initialize} for valid formats
146
+ # @param configuration [Configuration] the {Configuration} to be used for
147
+ # this instance
148
+ # @param timeout [Numeric] maximum time in seconds to connect
149
+ # @param exception [Class] exception class to be used when the connect timeout
150
+ # reached
151
+ #
152
+ # @return [self]
153
+ #
154
+ # @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
155
+ #
156
+ # @see NetworkError
157
+ #
158
+ def connect(address, configuration = nil, timeout: nil, exception: nil)
159
+ close if @socket
160
+ @address = Address.new(address)
161
+ @configuration = (configuration || Configuration.default).dup
162
+ raise(NoOpenSSLError) if @configuration.ssl? && !defined?(SSLSocket)
163
+ @socket = create_socket(timeout, exception)
164
+ self
59
165
  end
60
166
 
61
- def with_deadline(timeout)
62
- previous_deadline = @deadline
63
- raise(NoBlockGivenError) unless block_given?
64
- @deadline = Deadline.new(timeout)
65
- raise(InvalidDeadLineError, timeout) unless @deadline.valid?
66
- yield(self)
67
- ensure
68
- @deadline = previous_deadline
167
+ #
168
+ # Flush all internal buffers (write all through).
169
+ #
170
+ # @return [self]
171
+ #
172
+ def flush
173
+ stem_errors { @socket&.flush }
174
+ self
69
175
  end
70
176
 
177
+ #
178
+ # Read the given `nbytes` or the next available buffer from server.
179
+ #
180
+ # The optional `timeout` and `exception` parameters allow to override the
181
+ # `read_timeout` and `read_timeout_error` values of the used {#configuration}.
182
+ #
183
+ # @param nbytes [Integer] the number of bytes to read
184
+ # @param timeout [Numeric] maximum time in seconds to read
185
+ # @param exception [Class] exception class to be used when the read timeout
186
+ # reached
187
+ #
188
+ # @return [String] the read buffer
189
+ #
190
+ # @raise [NotConnectedError] if {#connect} was not called before
191
+ #
192
+ # @see NetworkError
193
+ #
71
194
  def read(nbytes = nil, timeout: nil, exception: nil)
72
195
  raise(NotConnectedError) if closed?
73
196
  deadline = create_deadline(timeout, configuration.read_timeout)
@@ -78,23 +201,77 @@ class TCPClient
78
201
  end
79
202
  end
80
203
 
81
- def write(*msg, timeout: nil, exception: nil)
204
+ #
205
+ # @return [String] the currently used address as text.
206
+ #
207
+ # @see Address#to_s
208
+ #
209
+ def to_s
210
+ @address&.to_s || ''
211
+ end
212
+
213
+ #
214
+ # Execute a block with a given overall time limit.
215
+ #
216
+ # When you like to ensure that a complete {#read}/{#write} communication
217
+ # sequence with the server is finished before a given amount of time you use
218
+ # this method.
219
+ #
220
+ # @example ensure to send SMTP welcome message and receive a 4 byte answer
221
+ # answer = client.with_deadline(2.5) do
222
+ # client.write('HELO')
223
+ # client.read(4)
224
+ # end
225
+ # # answer is EHLO when server speaks fluent SMPT
226
+ #
227
+ # @param timeout [Numeric] maximum time in seconds for all {#read} and
228
+ # {#write} calls within the block
229
+ #
230
+ # @yieldparam client [TCPClient] self
231
+ # @yieldreturn [Object] any result
232
+ #
233
+ # @return [Object] the block`s result
234
+ #
235
+ # @raise [NoBlockGivenError] if the block is missing
236
+ #
237
+ def with_deadline(timeout)
238
+ previous_deadline = @deadline
239
+ raise(NoBlockGivenError) unless block_given?
240
+ @deadline = Deadline.new(timeout)
241
+ raise(InvalidDeadLineError, timeout) unless @deadline.valid?
242
+ yield(self)
243
+ ensure
244
+ @deadline = previous_deadline
245
+ end
246
+
247
+ #
248
+ # Write the given `messages` to the server.
249
+ #
250
+ # The optional `timeout` and `exception` parameters allow to override the
251
+ # `write_timeout` and `write_timeout_error` values of the used
252
+ # {#configuration}.
253
+ #
254
+ # @param messages [String] one or more messages to write
255
+ # @param timeout [Numeric] maximum time in seconds to write
256
+ # @param exception [Class] exception class to be used when the write timeout
257
+ # reached
258
+ #
259
+ # @return [Integer] bytes written
260
+ #
261
+ # @raise [NotConnectedError] if {#connect} was not called before
262
+ #
263
+ def write(*messages, timeout: nil, exception: nil)
82
264
  raise(NotConnectedError) if closed?
83
265
  deadline = create_deadline(timeout, configuration.write_timeout)
84
- return stem_errors { @socket.write(*msg) } unless deadline.valid?
266
+ return stem_errors { @socket.write(*messages) } unless deadline.valid?
85
267
  exception ||= configuration.write_timeout_error
86
268
  stem_errors(exception) do
87
- msg.sum do |chunk|
269
+ messages.sum do |chunk|
88
270
  @socket.write_with_deadline(chunk.b, deadline, exception)
89
271
  end
90
272
  end
91
273
  end
92
274
 
93
- def flush
94
- stem_errors { @socket&.flush }
95
- self
96
- end
97
-
98
275
  private
99
276
 
100
277
  def create_deadline(timeout, default)
@@ -133,4 +310,5 @@ class TCPClient
133
310
  ].tap do |errors|
134
311
  errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
135
312
  end.freeze
313
+ private_constant(:NETWORK_ERRORS)
136
314
  end
data/rakefile.rb CHANGED
@@ -3,11 +3,10 @@
3
3
  require 'rake/clean'
4
4
  require 'bundler/gem_tasks'
5
5
  require 'rspec/core/rake_task'
6
+ require 'yard'
6
7
 
7
8
  $stdout.sync = $stderr.sync = true
8
-
9
- CLOBBER << 'prj'
10
-
9
+ CLOBBER << 'prj' << 'doc' << '.yardoc'
11
10
  task(:default) { exec('rake --tasks') }
12
-
13
11
  RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
12
+ YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
@@ -91,25 +91,12 @@ RSpec.describe TCPClient::Address do
91
91
  end
92
92
  end
93
93
 
94
- describe '#to_hash' do
95
- subject(:address) { TCPClient::Address.new('localhost:42') }
96
-
97
- it 'returns itself as an Hash' do
98
- expect(address.to_hash).to eq(host: 'localhost', port: 42)
99
- end
100
- end
101
-
102
94
  describe '#to_h' do
103
95
  subject(:address) { TCPClient::Address.new('localhost:42') }
104
96
 
105
97
  it 'returns itself as an Hash' do
106
98
  expect(address.to_h).to eq(host: 'localhost', port: 42)
107
99
  end
108
-
109
- it 'allows to specify the keys the result should contain' do
110
- expect(address.to_h(:port)).to eq(port: 42)
111
- expect(address.to_h(:host)).to eq(host: 'localhost')
112
- end
113
100
  end
114
101
 
115
102
  describe 'comparison' do
@@ -82,7 +82,7 @@ RSpec.describe TCPClient::Configuration do
82
82
  expect(configuration.keep_alive).to be false
83
83
  end
84
84
 
85
- it 'allows to configure reverse address lokup' do
85
+ it 'allows to configure reverse address lookup' do
86
86
  expect(configuration.reverse_lookup).to be false
87
87
  end
88
88
 
@@ -148,7 +148,7 @@ RSpec.describe TCPClient::Configuration do
148
148
  end
149
149
  end
150
150
 
151
- context 'with invalid attribte' do
151
+ context 'with invalid attribute' do
152
152
  it 'raises an error' do
153
153
  expect { TCPClient::Configuration.new(invalid: :value) }.to raise_error(
154
154
  TCPClient::UnknownAttributeError
@@ -157,39 +157,6 @@ RSpec.describe TCPClient::Configuration do
157
157
  end
158
158
  end
159
159
 
160
- describe '#to_hash' do
161
- subject(:configuration) do
162
- TCPClient::Configuration.new(
163
- buffered: false,
164
- connect_timeout: 1,
165
- read_timeout: 2,
166
- write_timeout: 3,
167
- ssl: {
168
- min_version: :TLS1_2,
169
- max_version: :TLS1_3
170
- }
171
- )
172
- end
173
-
174
- it 'returns itself as an Hash' do
175
- expect(configuration.to_hash).to eq(
176
- buffered: false,
177
- keep_alive: true,
178
- reverse_lookup: true,
179
- connect_timeout: 1,
180
- connect_timeout_error: TCPClient::ConnectTimeoutError,
181
- read_timeout: 2,
182
- read_timeout_error: TCPClient::ReadTimeoutError,
183
- write_timeout: 3,
184
- write_timeout_error: TCPClient::WriteTimeoutError,
185
- ssl_params: {
186
- min_version: :TLS1_2,
187
- max_version: :TLS1_3
188
- }
189
- )
190
- end
191
- end
192
-
193
160
  describe '#to_h' do
194
161
  subject(:configuration) do
195
162
  TCPClient::Configuration.new(
@@ -215,18 +182,13 @@ RSpec.describe TCPClient::Configuration do
215
182
  read_timeout_error: TCPClient::ReadTimeoutError,
216
183
  write_timeout: 3,
217
184
  write_timeout_error: TCPClient::WriteTimeoutError,
185
+ normalize_network_errors: false,
218
186
  ssl_params: {
219
187
  min_version: :TLS1_2,
220
188
  max_version: :TLS1_3
221
189
  }
222
190
  )
223
191
  end
224
-
225
- it 'allows to specify the keys the result should contain' do
226
- expect(
227
- configuration.to_h(:keep_alive, :read_timeout, :write_timeout)
228
- ).to eq(keep_alive: true, read_timeout: 2, write_timeout: 3)
229
- end
230
192
  end
231
193
 
232
194
  describe '#dup' do
data/tcp-client.gemspec CHANGED
@@ -5,12 +5,11 @@ require_relative './lib/tcp-client/version'
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'tcp-client'
7
7
  spec.version = TCPClient::VERSION
8
- spec.author = 'Mike Blumtritt'
9
-
10
8
  spec.required_ruby_version = '>= 2.7.0'
11
9
 
10
+ spec.author = 'Mike Blumtritt'
12
11
  spec.summary = 'A TCP client implementation with working timeout support.'
13
- spec.description = <<~DESCRIPTION
12
+ spec.description = <<~description
14
13
  This Gem implements a TCP client with (optional) SSL support.
15
14
  It is an easy to use, versatile configurable client that can correctly
16
15
  handle time limits.
@@ -18,21 +17,23 @@ Gem::Specification.new do |spec|
18
17
  predefined/configurable time limits for each method
19
18
  (`connect`, `read`, `write`). Deadlines for a sequence of read/write
20
19
  actions can also be monitored.
21
- DESCRIPTION
20
+ description
21
+
22
22
  spec.homepage = 'https://github.com/mblumtritt/tcp-client'
23
23
  spec.license = 'BSD-3-Clause'
24
-
25
- spec.metadata['source_code_uri'] = 'https://github.com/mblumtritt/tcp-client'
26
- spec.metadata['bug_tracker_uri'] =
27
- 'https://github.com/mblumtritt/tcp-client/issues'
24
+ spec.metadata.merge!(
25
+ 'source_code_uri' => 'https://github.com/mblumtritt/tcp-client',
26
+ 'bug_tracker_uri' => 'https://github.com/mblumtritt/tcp-client/issues',
27
+ 'documentation_uri' => 'https://rubydoc.info/github/mblumtritt/tcp-client'
28
+ )
28
29
 
29
30
  spec.add_development_dependency 'bundler'
30
31
  spec.add_development_dependency 'rake'
31
32
  spec.add_development_dependency 'rspec'
33
+ spec.add_development_dependency 'yard'
32
34
 
33
35
  all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
34
36
  spec.test_files = all_files.grep(%r{^spec/})
35
37
  spec.files = all_files - spec.test_files
36
-
37
38
  spec.extra_rdoc_files = %w[README.md LICENSE]
38
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-12 00:00:00.000000000 Z
11
+ date: 2021-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
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'
55
69
  description: |
56
70
  This Gem implements a TCP client with (optional) SSL support.
57
71
  It is an easy to use, versatile configurable client that can correctly
@@ -68,6 +82,7 @@ extra_rdoc_files:
68
82
  - LICENSE
69
83
  files:
70
84
  - ".gitignore"
85
+ - ".yardopts"
71
86
  - LICENSE
72
87
  - README.md
73
88
  - gems.rb
@@ -98,6 +113,7 @@ licenses:
98
113
  metadata:
99
114
  source_code_uri: https://github.com/mblumtritt/tcp-client
100
115
  bug_tracker_uri: https://github.com/mblumtritt/tcp-client/issues
116
+ documentation_uri: https://rubydoc.info/github/mblumtritt/tcp-client
101
117
  post_install_message:
102
118
  rdoc_options: []
103
119
  require_paths:
@@ -113,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
129
  - !ruby/object:Gem::Version
114
130
  version: '0'
115
131
  requirements: []
116
- rubygems_version: 3.2.28
132
+ rubygems_version: 3.2.32
117
133
  signing_key:
118
134
  specification_version: 4
119
135
  summary: A TCP client implementation with working timeout support.