http 6.0.0-java

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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -0
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require "http/errors"
6
+ require "http/headers/normalizer"
7
+ require "http/headers/known"
8
+
9
+ module HTTP
10
+ # HTTP Headers container.
11
+ class Headers
12
+ extend Forwardable
13
+ include Enumerable
14
+
15
+ class << self
16
+ # Coerces given object into Headers
17
+ #
18
+ # @example
19
+ # headers = HTTP::Headers.coerce("Content-Type" => "text/plain")
20
+ #
21
+ # @raise [Error] if object can't be coerced
22
+ # @param [#to_hash, #to_h, #to_a] object
23
+ # @return [Headers]
24
+ # @api public
25
+ def coerce(object)
26
+ object = if object.respond_to?(:to_hash) then object.to_hash
27
+ elsif object.respond_to?(:to_h) then object.to_h
28
+ elsif object.respond_to?(:to_a) then object.to_a
29
+ else raise Error, "Can't coerce #{object.inspect} to Headers"
30
+ end
31
+
32
+ headers = new
33
+ object.each { |k, v| headers.add k, v }
34
+ headers
35
+ end
36
+ # @!method [](object)
37
+ # Coerces given object into Headers
38
+ #
39
+ # @example
40
+ # headers = HTTP::Headers["Content-Type" => "text/plain"]
41
+ #
42
+ # @see .coerce
43
+ # @return [Headers]
44
+ # @api public
45
+ alias [] coerce
46
+
47
+ # Returns the shared normalizer instance
48
+ #
49
+ # @example
50
+ # HTTP::Headers.normalizer
51
+ #
52
+ # @return [Headers::Normalizer]
53
+ # @api public
54
+ def normalizer
55
+ @normalizer ||= Normalizer.new #: Headers::Normalizer
56
+ end
57
+ end
58
+
59
+ # Creates a new empty headers container
60
+ #
61
+ # @example
62
+ # headers = HTTP::Headers.new
63
+ #
64
+ # @return [Headers]
65
+ # @api public
66
+ def initialize
67
+ # The @pile stores each header value using a three element array:
68
+ # 0 - the normalized header key, used for lookup
69
+ # 1 - the header key as it will be sent with a request
70
+ # 2 - the value
71
+ @pile = []
72
+ end
73
+
74
+ # Sets header, replacing any existing values
75
+ #
76
+ # @example
77
+ # headers.set("Content-Type", "text/plain")
78
+ #
79
+ # @param (see #add)
80
+ # @return [void]
81
+ # @api public
82
+ def set(name, value)
83
+ delete(name)
84
+ add(name, value)
85
+ end
86
+ # @!method []=(name, value)
87
+ # Sets header, replacing any existing values
88
+ #
89
+ # @example
90
+ # headers["Content-Type"] = "text/plain"
91
+ #
92
+ # @see #set
93
+ # @return [void]
94
+ # @api public
95
+ alias []= set
96
+
97
+ # Removes header with the given name
98
+ #
99
+ # @example
100
+ # headers.delete("Content-Type")
101
+ #
102
+ # @param [#to_s] name header name
103
+ # @return [void]
104
+ # @api public
105
+ def delete(name)
106
+ name = normalize_header name
107
+ @pile.delete_if { |k, _| k.eql?(name) }
108
+ end
109
+
110
+ # Appends header value(s) to the given name
111
+ #
112
+ # @example
113
+ # headers.add("Accept", "text/html")
114
+ #
115
+ # @param [String, Symbol] name header name. When specified as a string, the
116
+ # name is sent as-is. When specified as a symbol, the name is converted
117
+ # to a string of capitalized words separated by a dash. Word boundaries
118
+ # are determined by an underscore (`_`) or a dash (`-`).
119
+ # Ex: `:content_type` is sent as `"Content-Type"`, and `"auth_key"` (string)
120
+ # is sent as `"auth_key"`.
121
+ # @param [Array<#to_s>, #to_s] value header value(s) to be appended
122
+ # @return [void]
123
+ # @api public
124
+ def add(name, value)
125
+ lookup_name = normalize_header(name)
126
+ wire_name = wire_name_for(name, lookup_name)
127
+
128
+ Array(value).each do |v|
129
+ @pile << [
130
+ lookup_name,
131
+ wire_name,
132
+ validate_value(v)
133
+ ]
134
+ end
135
+ end
136
+
137
+ # Returns list of header values if any
138
+ #
139
+ # @example
140
+ # headers.get("Content-Type")
141
+ #
142
+ # @return [Array<String>]
143
+ # @api public
144
+ def get(name)
145
+ name = normalize_header name
146
+ @pile.filter_map { |k, _, v| v if k.eql?(name) }
147
+ end
148
+
149
+ # Smart version of {#get}
150
+ #
151
+ # @example
152
+ # headers["Content-Type"]
153
+ #
154
+ # @return [nil] if header was not set
155
+ # @return [String] if header has exactly one value
156
+ # @return [Array<String>] if header has more than one value
157
+ # @api public
158
+ def [](name)
159
+ values = get(name)
160
+ return if values.empty?
161
+
162
+ return values unless values.one?
163
+
164
+ values.join
165
+ end
166
+
167
+ # Tells whether header with given name is set
168
+ #
169
+ # @example
170
+ # headers.include?("Content-Type")
171
+ #
172
+ # @return [Boolean]
173
+ # @api public
174
+ def include?(name)
175
+ name = normalize_header name
176
+ @pile.any? { |k, _| k.eql?(name) }
177
+ end
178
+
179
+ # Returns Rack-compatible headers Hash
180
+ #
181
+ # @example
182
+ # headers.to_h
183
+ #
184
+ # @return [Hash]
185
+ # @api public
186
+ def to_h
187
+ keys.to_h { |k| [k, self[k]] }
188
+ end
189
+ # @!method to_hash
190
+ # @see #to_h
191
+ # @return [Hash]
192
+ alias to_hash to_h
193
+
194
+ # Pattern matching interface
195
+ #
196
+ # @example
197
+ # headers.deconstruct_keys(%i[content_type])
198
+ #
199
+ # @param keys [Array<Symbol>, nil] keys to extract, or nil for all
200
+ # @return [Hash{Symbol => Object}]
201
+ # @api public
202
+ def deconstruct_keys(keys)
203
+ hash = @pile.map { |_, k, _| k }.to_h { |k| [k.tr("A-Z-", "a-z_").to_sym, self[k]] }
204
+ keys ? hash.slice(*keys) : hash
205
+ end
206
+
207
+ # Returns human-readable representation of self instance
208
+ #
209
+ # @example
210
+ # headers.inspect
211
+ #
212
+ # @return [String]
213
+ # @api public
214
+ def inspect = "#<#{self.class}>"
215
+
216
+ # Returns list of header names
217
+ #
218
+ # @example
219
+ # headers.keys
220
+ #
221
+ # @return [Array<String>]
222
+ # @api public
223
+ def keys
224
+ @pile.map { |_, k, _| k }.uniq
225
+ end
226
+
227
+ # Compares headers to another Headers or Array of pairs
228
+ #
229
+ # @example
230
+ # headers == other_headers
231
+ #
232
+ # @return [Boolean]
233
+ # @api public
234
+ def ==(other)
235
+ return false unless other.respond_to? :to_a
236
+
237
+ to_a.eql?(other.to_a)
238
+ end
239
+
240
+ # Calls the given block once for each key/value pair
241
+ #
242
+ # @example
243
+ # headers.each { |name, value| puts "#{name}: #{value}" }
244
+ #
245
+ # @return [Enumerator] if no block given
246
+ # @return [Headers] self-reference
247
+ # @api public
248
+ def each
249
+ return to_enum unless block_given?
250
+
251
+ @pile.each { |item| yield(item.drop(1)) }
252
+ self
253
+ end
254
+
255
+ # @!method empty?
256
+ # Returns true if self has no key/value pairs
257
+ #
258
+ # @example
259
+ # headers.empty?
260
+ #
261
+ # @return [Boolean]
262
+ # @api public
263
+ def_delegator :@pile, :empty?
264
+
265
+ # @!method hash
266
+ # Computes a hash-code for this headers container
267
+ #
268
+ # @example
269
+ # headers.hash
270
+ #
271
+ # @see http://www.ruby-doc.org/core/Object.html#method-i-hash
272
+ # @return [Fixnum]
273
+ # @api public
274
+ def_delegator :@pile, :hash
275
+
276
+ # Properly clones internal key/value storage
277
+ #
278
+ # @return [void]
279
+ # @api private
280
+ def initialize_copy(_orig)
281
+ @pile = @pile.map(&:dup)
282
+ end
283
+
284
+ # Merges other headers into self
285
+ #
286
+ # @example
287
+ # headers.merge!("Accept" => "text/html")
288
+ #
289
+ # @see #merge
290
+ # @return [void]
291
+ # @api public
292
+ def merge!(other)
293
+ coerced = self.class.coerce(other)
294
+ names = coerced.keys
295
+ names.each { |name| set name, coerced.get(name) }
296
+ end
297
+
298
+ # Returns new instance with other headers merged in
299
+ #
300
+ # @example
301
+ # new_headers = headers.merge("Accept" => "text/html")
302
+ #
303
+ # @see #merge!
304
+ # @return [Headers]
305
+ # @api public
306
+ def merge(other)
307
+ dup.tap { |dupped| dupped.merge! other }
308
+ end
309
+
310
+ private
311
+
312
+ # Returns the wire name for a header
313
+ #
314
+ # @return [String]
315
+ # @api private
316
+ def wire_name_for(name, lookup_name)
317
+ case name
318
+ when String then name
319
+ when Symbol then lookup_name
320
+ else raise HeaderError, "HTTP header must be a String or Symbol: #{name.inspect}"
321
+ end
322
+ end
323
+
324
+ # Transforms name to canonical HTTP header capitalization
325
+ #
326
+ # @return [String]
327
+ # @api private
328
+ def normalize_header(name) = self.class.normalizer.call(name)
329
+
330
+ # Ensures there is no new line character in the header value
331
+ #
332
+ # @param [String] value
333
+ # @raise [HeaderError] if value includes new line character
334
+ # @return [String] stringified header value
335
+ # @api private
336
+ def validate_value(value)
337
+ v = value.to_s
338
+ return v unless v.include?("\n") || v.include?("\r")
339
+
340
+ raise HeaderError, "Invalid HTTP header field value: #{v.inspect}"
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "singleton"
5
+
6
+ module HTTP
7
+ module MimeType
8
+ # Base encode/decode MIME type adapter
9
+ class Adapter
10
+ include Singleton
11
+
12
+ class << self
13
+ extend Forwardable
14
+
15
+ def_delegators :instance, :encode, :decode # steep:ignore
16
+ end
17
+
18
+ # Encodes data into the MIME type format
19
+ #
20
+ # @example
21
+ # adapter.encode("foo" => "bar")
22
+ #
23
+ # @return [String] encoded representation
24
+ # @raise [Error] if not implemented by subclass
25
+ # @api public
26
+ def encode(*)
27
+ raise Error, "#{self.class} does not supports #encode"
28
+ end
29
+
30
+ # Decodes data from the MIME type format
31
+ #
32
+ # @example
33
+ # adapter.decode("{\"foo\":\"bar\"}")
34
+ #
35
+ # @return [Object] decoded data
36
+ # @raise [Error] if not implemented by subclass
37
+ # @api public
38
+ def decode(*)
39
+ raise Error, "#{self.class} does not supports #decode"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "http/mime_type/adapter"
5
+
6
+ module HTTP
7
+ # MIME type registry and adapter interface
8
+ module MimeType
9
+ # JSON encode/decode MIME type adapter
10
+ class JSON < Adapter
11
+ # Encodes object to JSON
12
+ #
13
+ # @example
14
+ # adapter = HTTP::MimeType::JSON.new
15
+ # adapter.encode(foo: "bar")
16
+ #
17
+ # @param [Object] obj object to encode
18
+ # @api public
19
+ # @return [String]
20
+ def encode(obj)
21
+ obj.to_json
22
+ end
23
+
24
+ # Decodes JSON string into Ruby object
25
+ #
26
+ # @example
27
+ # adapter = HTTP::MimeType::JSON.new
28
+ # adapter.decode('{"foo":"bar"}')
29
+ #
30
+ # @param [String] str JSON string to decode
31
+ # @api public
32
+ # @return [Object]
33
+ def decode(str)
34
+ ::JSON.parse str
35
+ end
36
+ end
37
+
38
+ register_adapter "application/json", JSON
39
+ register_alias "application/json", :json
40
+ end
41
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/errors"
4
+
5
+ module HTTP
6
+ # MIME type encode/decode adapters
7
+ module MimeType
8
+ class << self
9
+ # Associate MIME type with adapter
10
+ #
11
+ # @example
12
+ #
13
+ # module JsonAdapter
14
+ # class << self
15
+ # def encode(obj)
16
+ # # encode logic here
17
+ # end
18
+ #
19
+ # def decode(str)
20
+ # # decode logic here
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # HTTP::MimeType.register_adapter 'application/json', MyJsonAdapter
26
+ #
27
+ # @param [#to_s] type
28
+ # @param [#encode, #decode] adapter
29
+ # @api public
30
+ # @return [void]
31
+ def register_adapter(type, adapter)
32
+ adapters[type.to_s] = adapter
33
+ end
34
+
35
+ # Returns adapter associated with MIME type
36
+ #
37
+ # @example
38
+ # HTTP::MimeType["application/json"]
39
+ #
40
+ # @param [#to_s] type
41
+ # @raise [Error] if no adapter found
42
+ # @api public
43
+ # @return [Class]
44
+ def [](type)
45
+ adapters[normalize type] || raise(UnsupportedMimeTypeError, "Unknown MIME type: #{type}")
46
+ end
47
+
48
+ # Register a shortcut for MIME type
49
+ #
50
+ # @example
51
+ #
52
+ # HTTP::MimeType.register_alias 'application/json', :json
53
+ #
54
+ # @param [#to_s] type
55
+ # @param [#to_sym] shortcut
56
+ # @api public
57
+ # @return [void]
58
+ def register_alias(type, shortcut)
59
+ aliases[shortcut.to_sym] = type.to_s
60
+ end
61
+
62
+ # Resolves type by shortcut if possible
63
+ #
64
+ # @example
65
+ # HTTP::MimeType.normalize(:json)
66
+ #
67
+ # @param [#to_s] type
68
+ # @api public
69
+ # @return [String]
70
+ def normalize(type)
71
+ aliases.fetch type, type.to_s
72
+ end
73
+
74
+ private
75
+
76
+ # Returns the adapters registry hash
77
+ #
78
+ # @api private
79
+ # @return [Hash]
80
+ def adapters
81
+ @adapters ||= {}
82
+ end
83
+
84
+ # Returns the aliases registry hash
85
+ #
86
+ # @api private
87
+ # @return [Hash]
88
+ def aliases
89
+ @aliases ||= {}
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # built-in mime types
96
+ require "http/mime_type/json"
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Configuration options for HTTP requests and clients
5
+ class Options
6
+ def_option :headers do |new_headers|
7
+ headers.merge(new_headers) # steep:ignore
8
+ end
9
+
10
+ def_option :encoding do |encoding|
11
+ self.encoding = Encoding.find(encoding) # steep:ignore
12
+ end
13
+
14
+ def_option :features, reader_only: true do |new_features|
15
+ # Normalize features from:
16
+ #
17
+ # [{feature_one: {opt: 'val'}}, :feature_two]
18
+ #
19
+ # into:
20
+ #
21
+ # {feature_one: {opt: 'val'}, feature_two: {}}
22
+ acc = {} #: Hash[untyped, untyped]
23
+ normalized_features = new_features.each_with_object(acc) do |feature, h|
24
+ if feature.is_a?(Hash)
25
+ h.merge!(feature)
26
+ else
27
+ h[feature] = {} # steep:ignore
28
+ end
29
+ end
30
+
31
+ features.merge(normalized_features) # steep:ignore
32
+ end
33
+
34
+ # Sets and normalizes features hash
35
+ #
36
+ # @param [Hash] features
37
+ # @api private
38
+ # @return [Hash]
39
+ def features=(features)
40
+ result = {} #: Hash[Symbol, Feature]
41
+ @features = features.each_with_object(result) do |(name, opts_or_feature), h|
42
+ h[name] = if opts_or_feature.is_a?(Feature)
43
+ opts_or_feature
44
+ else
45
+ unless (feature = self.class.available_features[name])
46
+ argument_error! "Unsupported feature: #{name}"
47
+ end
48
+ feature.new(**opts_or_feature) # steep:ignore
49
+ end
50
+ end
51
+ end
52
+
53
+ %w[
54
+ proxy params form json body response
55
+ socket_class nodelay ssl_socket_class ssl_context ssl
56
+ keep_alive_timeout timeout_class timeout_options
57
+ ].each do |method_name|
58
+ def_option method_name
59
+ end
60
+
61
+ def_option :follow, reader_only: true
62
+
63
+ # Sets follow redirect options
64
+ #
65
+ # @param [Boolean, Hash, nil] value
66
+ # @api private
67
+ # @return [Hash, nil]
68
+ def follow=(value)
69
+ @follow =
70
+ if !value then nil
71
+ elsif true == value then {} #: Hash[untyped, untyped]
72
+ elsif value.respond_to?(:fetch) then value
73
+ else argument_error! "Unsupported follow options: #{value}"
74
+ end
75
+ end
76
+
77
+ def_option :retriable, reader_only: true
78
+
79
+ # Sets retriable options
80
+ #
81
+ # @param [Boolean, Hash, nil] value
82
+ # @api private
83
+ # @return [Hash, nil]
84
+ def retriable=(value)
85
+ @retriable =
86
+ if !value then nil
87
+ elsif true == value then {} #: Hash[untyped, untyped]
88
+ elsif value.respond_to?(:fetch) then value
89
+ else argument_error! "Unsupported retriable options: #{value}"
90
+ end
91
+ end
92
+
93
+ def_option :base_uri, reader_only: true
94
+
95
+ # Sets the base URI for resolving relative request paths
96
+ #
97
+ # @param [String, HTTP::URI, nil] value
98
+ # @api private
99
+ # @return [HTTP::URI, nil]
100
+ def base_uri=(value)
101
+ @base_uri = value ? parse_base_uri(value) : nil
102
+ validate_base_uri_and_persistent!
103
+ end
104
+
105
+ # Checks whether a base URI is set
106
+ #
107
+ # @example
108
+ # opts = HTTP::Options.new(base_uri: "https://example.com")
109
+ # opts.base_uri?
110
+ #
111
+ # @api public
112
+ # @return [Boolean]
113
+ def base_uri?
114
+ !base_uri.nil?
115
+ end
116
+
117
+ def_option :persistent, reader_only: true
118
+
119
+ # Sets persistent connection origin
120
+ #
121
+ # @param [String, nil] value
122
+ # @api private
123
+ # @return [String, nil]
124
+ def persistent=(value)
125
+ @persistent = value ? URI.parse(value).origin : nil
126
+ validate_base_uri_and_persistent!
127
+ end
128
+
129
+ # Checks whether persistent connection is enabled
130
+ #
131
+ # @example
132
+ # opts = HTTP::Options.new(persistent: "http://example.com")
133
+ # opts.persistent?
134
+ #
135
+ # @api public
136
+ # @return [Boolean]
137
+ def persistent?
138
+ !persistent.nil?
139
+ end
140
+
141
+ private
142
+
143
+ # Parses and validates a base URI value
144
+ #
145
+ # @param [String, HTTP::URI] value the base URI to parse
146
+ # @api private
147
+ # @return [HTTP::URI]
148
+ def parse_base_uri(value)
149
+ uri = URI.parse(value)
150
+
151
+ base = @base_uri
152
+ return resolve_base_uri(base, uri) if base
153
+
154
+ argument_error!(format("Invalid base URI: %s", value)) unless uri.scheme
155
+ uri
156
+ end
157
+
158
+ # Resolves a relative URI against an existing base URI
159
+ #
160
+ # @param [HTTP::URI] base the existing base URI
161
+ # @param [HTTP::URI] relative the URI to join
162
+ # @api private
163
+ # @return [HTTP::URI]
164
+ def resolve_base_uri(base, relative)
165
+ unless base.path.end_with?("/")
166
+ base = base.dup
167
+ base.path = "#{base.path}/"
168
+ end
169
+
170
+ URI.parse(base.join(relative))
171
+ end
172
+
173
+ # Validates that base URI and persistent origin are compatible
174
+ #
175
+ # @api private
176
+ # @return [void]
177
+ def validate_base_uri_and_persistent!
178
+ base = @base_uri
179
+ persistent = @persistent
180
+ return unless base && persistent
181
+ return if base.origin.eql?(persistent)
182
+
183
+ argument_error!(
184
+ format("Persistence origin (%s) conflicts with base URI origin (%s)",
185
+ persistent, base.origin)
186
+ )
187
+ end
188
+ end
189
+ end