tcp-client 0.8.0 → 0.9.3

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: 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.