typhoeus 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +217 -1
- data/README.md +338 -160
- data/UPGRADE.md +29 -0
- data/lib/rack/typhoeus/middleware/params_decoder/helper.rb +67 -14
- data/lib/rack/typhoeus/middleware/params_decoder.rb +8 -19
- data/lib/rack/typhoeus.rb +2 -0
- data/lib/typhoeus/adapters/faraday.rb +89 -28
- data/lib/typhoeus/cache/dalli.rb +2 -0
- data/lib/typhoeus/cache/rails.rb +3 -1
- data/lib/typhoeus/cache/redis.rb +2 -0
- data/lib/typhoeus/config.rb +51 -7
- data/lib/typhoeus/easy_factory.rb +129 -23
- data/lib/typhoeus/errors/invalid_option.rb +23 -0
- data/lib/typhoeus/errors/no_response.rb +18 -0
- data/lib/typhoeus/errors/no_stub.rb +8 -1
- data/lib/typhoeus/errors/typhoeus_error.rb +2 -0
- data/lib/typhoeus/errors.rb +4 -0
- data/lib/typhoeus/expectation.rb +33 -1
- data/lib/typhoeus/hydra/queueable.rb +29 -4
- data/lib/typhoeus/hydra.rb +349 -21
- data/lib/typhoeus/pool.rb +80 -17
- data/lib/typhoeus/railtie.rb +4 -2
- data/lib/typhoeus/request/actions.rb +2 -0
- data/lib/typhoeus/request/callbacks.rb +11 -4
- data/lib/typhoeus/request/marshal.rb +5 -2
- data/lib/typhoeus/request/streamable.rb +2 -0
- data/lib/typhoeus/request.rb +317 -30
- data/lib/typhoeus/response/cacheable.rb +2 -0
- data/lib/typhoeus/response/header.rb +234 -14
- data/lib/typhoeus/response/informations.rb +75 -12
- data/lib/typhoeus/response/status.rb +4 -13
- data/lib/typhoeus/response.rb +35 -0
- data/lib/typhoeus/version.rb +3 -1
- data/lib/typhoeus.rb +89 -26
- data/typhoeus.gemspec +15 -3
- metadata +19 -22
- data/lib/typhoeus/hydra/addable.rb +0 -23
- data/lib/typhoeus/hydra/before.rb +0 -31
- data/lib/typhoeus/hydra/block_connection.rb +0 -35
- data/lib/typhoeus/hydra/cacheable.rb +0 -15
- data/lib/typhoeus/hydra/memoizable.rb +0 -56
- data/lib/typhoeus/hydra/runnable.rb +0 -19
- data/lib/typhoeus/hydra/stubbable.rb +0 -28
- data/lib/typhoeus/request/before.rb +0 -30
- data/lib/typhoeus/request/block_connection.rb +0 -52
- data/lib/typhoeus/request/cacheable.rb +0 -38
- data/lib/typhoeus/request/memoizable.rb +0 -38
- data/lib/typhoeus/request/operations.rb +0 -40
- data/lib/typhoeus/request/responseable.rb +0 -29
- data/lib/typhoeus/request/stubbable.rb +0 -30
data/lib/typhoeus/request.rb
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zlib'
|
|
2
4
|
require 'digest/sha1'
|
|
3
5
|
require 'typhoeus/request/actions'
|
|
4
|
-
require 'typhoeus/request/before'
|
|
5
|
-
require 'typhoeus/request/block_connection'
|
|
6
|
-
require 'typhoeus/request/cacheable'
|
|
7
6
|
require 'typhoeus/request/callbacks'
|
|
8
7
|
require 'typhoeus/request/marshal'
|
|
9
|
-
require 'typhoeus/request/memoizable'
|
|
10
|
-
require 'typhoeus/request/operations'
|
|
11
|
-
require 'typhoeus/request/responseable'
|
|
12
8
|
require 'typhoeus/request/streamable'
|
|
13
|
-
require 'typhoeus/request/stubbable'
|
|
14
9
|
|
|
15
10
|
module Typhoeus
|
|
16
11
|
|
|
@@ -23,18 +18,11 @@ module Typhoeus
|
|
|
23
18
|
#
|
|
24
19
|
# @see (see #initialize)
|
|
25
20
|
class Request
|
|
26
|
-
extend
|
|
21
|
+
extend Request::Actions
|
|
27
22
|
include Request::Callbacks::Types
|
|
28
23
|
include Request::Callbacks
|
|
29
24
|
include Request::Streamable
|
|
30
25
|
include Request::Marshal
|
|
31
|
-
include Request::Operations
|
|
32
|
-
include Request::Responseable
|
|
33
|
-
include Request::Memoizable
|
|
34
|
-
include Request::Cacheable
|
|
35
|
-
include Request::BlockConnection
|
|
36
|
-
include Request::Stubbable
|
|
37
|
-
include Request::Before
|
|
38
26
|
|
|
39
27
|
# Returns the provided base url.
|
|
40
28
|
#
|
|
@@ -118,6 +106,184 @@ module Typhoeus
|
|
|
118
106
|
set_defaults
|
|
119
107
|
end
|
|
120
108
|
|
|
109
|
+
# Run the request.
|
|
110
|
+
#
|
|
111
|
+
# The before, stub, block connection, and cache checks run here explicitly
|
|
112
|
+
# instead of relying on the order of included modules.
|
|
113
|
+
#
|
|
114
|
+
# @example Make a request.
|
|
115
|
+
# Typhoeus::Request.new("www.example.com").run
|
|
116
|
+
#
|
|
117
|
+
# @return [ Response ] The response.
|
|
118
|
+
def run
|
|
119
|
+
callbacks = Typhoeus.before_callbacks_for(self)
|
|
120
|
+
if callbacks
|
|
121
|
+
callbacks.each do |callback|
|
|
122
|
+
value = callback.call(self)
|
|
123
|
+
if value.is_a?(Response)
|
|
124
|
+
unless response
|
|
125
|
+
# A synthetic response is a short circuit like a stub. Mark it
|
|
126
|
+
# so success? works and it is not written to the cache.
|
|
127
|
+
value.mock = true
|
|
128
|
+
execute_headers_callbacks(value)
|
|
129
|
+
on_body.each { |body_callback| body_callback.call(value.body, value) }
|
|
130
|
+
finish(value)
|
|
131
|
+
end
|
|
132
|
+
return value
|
|
133
|
+
end
|
|
134
|
+
return response if value.nil? || value == false
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unless Typhoeus.stubs_disabled?
|
|
139
|
+
if stub_response = Expectation.response_for(self)
|
|
140
|
+
# The stub block already ran this request for real.
|
|
141
|
+
return stub_response if stub_response.request.equal?(self)
|
|
142
|
+
|
|
143
|
+
execute_headers_callbacks(stub_response)
|
|
144
|
+
on_body.each { |callback| callback.call(stub_response.body, stub_response) }
|
|
145
|
+
return finish(stub_response)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
raise Typhoeus::Errors::NoStub.new(self) if blocked?
|
|
150
|
+
|
|
151
|
+
if cached = cached_response
|
|
152
|
+
return finish_cached_response(cached)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
perform
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Add a callback that runs before the request.
|
|
159
|
+
#
|
|
160
|
+
# @example Add a callback.
|
|
161
|
+
# request.before { |request| request.options[:timeout] = 5 }
|
|
162
|
+
#
|
|
163
|
+
# @param [ Proc ] block The callback to run.
|
|
164
|
+
#
|
|
165
|
+
# @yield [ Typhoeus::Request ]
|
|
166
|
+
#
|
|
167
|
+
# @return [ Array<Proc> ] The callbacks.
|
|
168
|
+
def before(&block)
|
|
169
|
+
@before ||= []
|
|
170
|
+
@before << block if block_given?
|
|
171
|
+
@before
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Returns whether a request is blocked. Takes request.block_connection and
|
|
175
|
+
# the connection state of the current thread into consideration.
|
|
176
|
+
#
|
|
177
|
+
# @example Blocked?
|
|
178
|
+
# request.blocked?
|
|
179
|
+
#
|
|
180
|
+
# @return [ Boolean ] True if blocked, false else.
|
|
181
|
+
def blocked?
|
|
182
|
+
if block_connection.nil?
|
|
183
|
+
Typhoeus.connection_blocked?
|
|
184
|
+
else
|
|
185
|
+
block_connection
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Return whether a request is memoizable.
|
|
190
|
+
#
|
|
191
|
+
# Streaming requests are not memoized because their body is not retained
|
|
192
|
+
# and every caller has to receive its own chunks.
|
|
193
|
+
#
|
|
194
|
+
# @example Is request memoizable?
|
|
195
|
+
# request.memoizable?
|
|
196
|
+
#
|
|
197
|
+
# @return [ Boolean ] Return true if memoizable, false else.
|
|
198
|
+
def memoizable?
|
|
199
|
+
Typhoeus::Config.memoize &&
|
|
200
|
+
!streaming? &&
|
|
201
|
+
request_method == :get
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Return whether a request is cacheable.
|
|
205
|
+
#
|
|
206
|
+
# @return [ Boolean ]
|
|
207
|
+
def cacheable?
|
|
208
|
+
cache
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Return the cached response for the request, if there is one.
|
|
212
|
+
#
|
|
213
|
+
# @return [ Typhoeus::Response, nil ]
|
|
214
|
+
#
|
|
215
|
+
# @api private
|
|
216
|
+
def cached_response
|
|
217
|
+
cacheable? && cache.get(self)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Finish a response that came from the cache and deliver it through the
|
|
221
|
+
# same callbacks a stubbed response uses.
|
|
222
|
+
#
|
|
223
|
+
# @param [ Typhoeus::Response ] response The cached response.
|
|
224
|
+
#
|
|
225
|
+
# @return [ Typhoeus::Response ] The response.
|
|
226
|
+
#
|
|
227
|
+
# @api private
|
|
228
|
+
def finish_cached_response(response)
|
|
229
|
+
copy = response.dup
|
|
230
|
+
copy.cached = true
|
|
231
|
+
copy.request = self
|
|
232
|
+
execute_headers_callbacks(copy)
|
|
233
|
+
on_body.each { |callback| callback.call(copy.body, copy) }
|
|
234
|
+
finish(copy)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Return the cache TTL for the request.
|
|
238
|
+
#
|
|
239
|
+
# @return [ Integer, nil ]
|
|
240
|
+
def cache_ttl
|
|
241
|
+
options[:cache_ttl]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Set the response and execute the callbacks.
|
|
245
|
+
#
|
|
246
|
+
# @param [ Typhoeus::Response ] response The response.
|
|
247
|
+
# @param [ Boolean ] bypass_memoization Whether to bypass memoization.
|
|
248
|
+
#
|
|
249
|
+
# @return [ Typhoeus::Response ] The response.
|
|
250
|
+
def finish(response, bypass_memoization = nil)
|
|
251
|
+
if bypass_memoization
|
|
252
|
+
@response = response
|
|
253
|
+
else
|
|
254
|
+
self.response = response
|
|
255
|
+
end
|
|
256
|
+
self.response.request = self
|
|
257
|
+
execute_callbacks
|
|
258
|
+
response
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Return the response.
|
|
262
|
+
#
|
|
263
|
+
# @return [ Typhoeus::Response, nil ]
|
|
264
|
+
def response
|
|
265
|
+
@response ||= nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Set the response. Cache and memoization hooks run explicitly before the
|
|
269
|
+
# response is assigned, instead of relying on the order modules were
|
|
270
|
+
# included in.
|
|
271
|
+
#
|
|
272
|
+
# @param [ Typhoeus::Response ] value The response to set.
|
|
273
|
+
def response=(value)
|
|
274
|
+
if cacheable? && cacheable_response?(value)
|
|
275
|
+
# The request exposes this response to its callbacks and the caller.
|
|
276
|
+
# Store a snapshot so later mutation cannot change what a cache hit
|
|
277
|
+
# returns.
|
|
278
|
+
cache.set(self, value.dup)
|
|
279
|
+
end
|
|
280
|
+
# Followers receive a copy of the stored response, and the leader's
|
|
281
|
+
# callbacks must not change what they see. Store a snapshot instead of
|
|
282
|
+
# the response the leader exposes to its own callbacks.
|
|
283
|
+
hydra.memory[self] = value.dup if memoizable? && hydra
|
|
284
|
+
@response = value
|
|
285
|
+
end
|
|
286
|
+
|
|
121
287
|
# Return the url.
|
|
122
288
|
# In contrast to base_url which returns the value you specified, url returns
|
|
123
289
|
# the full url including the parameters.
|
|
@@ -127,10 +293,7 @@ module Typhoeus
|
|
|
127
293
|
#
|
|
128
294
|
# @since 0.5.5
|
|
129
295
|
def url
|
|
130
|
-
|
|
131
|
-
url = easy.url
|
|
132
|
-
Typhoeus::Pool.release(easy)
|
|
133
|
-
url
|
|
296
|
+
EasyFactory.new(self).url
|
|
134
297
|
end
|
|
135
298
|
|
|
136
299
|
# Returns whether other is equal to self.
|
|
@@ -145,8 +308,8 @@ module Typhoeus
|
|
|
145
308
|
# @api private
|
|
146
309
|
def eql?(other)
|
|
147
310
|
self.class == other.class &&
|
|
148
|
-
|
|
149
|
-
fuzzy_hash_eql?(
|
|
311
|
+
base_url.to_s == other.base_url.to_s &&
|
|
312
|
+
fuzzy_hash_eql?(normalized_options, other.normalized_options)
|
|
150
313
|
end
|
|
151
314
|
|
|
152
315
|
# Overrides Object#hash.
|
|
@@ -163,22 +326,118 @@ module Typhoeus
|
|
|
163
326
|
#
|
|
164
327
|
# @return [ String ] The cache key.
|
|
165
328
|
def cache_key
|
|
166
|
-
|
|
329
|
+
# The cache store and the TTL only affect where and how long a response
|
|
330
|
+
# is stored, not what the response is. Leaving them in the key would
|
|
331
|
+
# silently miss the cache whenever the TTL changes.
|
|
332
|
+
hashable_options = normalized_options.tap do |dup_opts|
|
|
333
|
+
dup_opts.delete(:cache)
|
|
334
|
+
dup_opts.delete(:cache_ttl)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
cache_key_components = [
|
|
338
|
+
self.class.name,
|
|
339
|
+
base_url,
|
|
340
|
+
hashable_string_for(hashable_options)
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
Digest::SHA1.hexdigest(cache_key_components.join)
|
|
167
344
|
end
|
|
168
345
|
|
|
169
|
-
# Mimics
|
|
346
|
+
# Mimics libcurl's POST body generation. This is not accurate, but good
|
|
170
347
|
# enough for VCR.
|
|
171
348
|
#
|
|
172
349
|
# @return [ String ] The encoded body.
|
|
173
|
-
# otherwise.
|
|
174
350
|
#
|
|
175
351
|
# @api private
|
|
176
352
|
def encoded_body
|
|
177
|
-
|
|
353
|
+
easy = Typhoeus::Pool.get
|
|
354
|
+
begin
|
|
355
|
+
body_options = { :body => options[:body] }
|
|
356
|
+
[:params_encoding, :escape].each do |option|
|
|
357
|
+
body_options[option] = options[option] if options.key?(option)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Let Ethon apply its escape rule for this method. The form must stay
|
|
361
|
+
# non-multipart because encoded_body returns a single string.
|
|
362
|
+
form = Ethon::Easy::Form.new(easy, options[:body])
|
|
363
|
+
form.define_singleton_method(:multipart?) { false }
|
|
364
|
+
|
|
365
|
+
action = if options.fetch(:method, :get).to_s.downcase == "put"
|
|
366
|
+
Ethon::Easy::Http::Put.new("http://localhost/", body_options)
|
|
367
|
+
else
|
|
368
|
+
Ethon::Easy::Http::Post.new("http://localhost/", body_options)
|
|
369
|
+
end
|
|
370
|
+
action.instance_variable_set(:@form, form)
|
|
371
|
+
action.setup(easy)
|
|
372
|
+
form.to_s
|
|
373
|
+
ensure
|
|
374
|
+
Typhoeus::Pool.release(easy) if easy
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Return the request method with the same defaults and spelling Ethon
|
|
379
|
+
# uses. Cache keys, memoization, and equality must not depend on whether
|
|
380
|
+
# the caller wrote :get, "GET", or omitted the default method.
|
|
381
|
+
#
|
|
382
|
+
# @return [ Symbol ] The normalized method.
|
|
383
|
+
#
|
|
384
|
+
# @api private
|
|
385
|
+
def request_method
|
|
386
|
+
(options[:method] || :get).to_s.downcase.to_sym
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Return the options with their implicit defaults filled in for comparison.
|
|
390
|
+
#
|
|
391
|
+
# @return [ Hash ] The normalized options.
|
|
392
|
+
#
|
|
393
|
+
# @api private
|
|
394
|
+
def normalized_options
|
|
395
|
+
normalized = options.merge(:method => request_method)
|
|
396
|
+
normalized.delete("method")
|
|
397
|
+
# GET is the default. Leaving it out keeps the cache key of an implicit
|
|
398
|
+
# GET stable across the normalization change.
|
|
399
|
+
normalized.delete(:method) if normalized[:method] == :get
|
|
400
|
+
normalized
|
|
178
401
|
end
|
|
179
402
|
|
|
180
403
|
private
|
|
181
404
|
|
|
405
|
+
# Perform the actual request.
|
|
406
|
+
#
|
|
407
|
+
# @return [ Typhoeus::Response ] The response.
|
|
408
|
+
def perform
|
|
409
|
+
easy_factory = EasyFactory.new(self)
|
|
410
|
+
easy = easy_factory.get
|
|
411
|
+
begin
|
|
412
|
+
easy.perform
|
|
413
|
+
rescue Exception
|
|
414
|
+
easy_factory.release_easy(easy) unless easy_factory.released?
|
|
415
|
+
raise
|
|
416
|
+
end
|
|
417
|
+
response
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Return the cache for the request, falling back to the global cache.
|
|
421
|
+
#
|
|
422
|
+
# @return [ Object, nil ]
|
|
423
|
+
def cache
|
|
424
|
+
return nil if options[:cache] === false
|
|
425
|
+
options[:cache] || Typhoeus::Config.cache
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# A streaming response has no buffered body, and a transport failure or a
|
|
429
|
+
# stub is not a representation of the resource.
|
|
430
|
+
#
|
|
431
|
+
# @param [ Typhoeus::Response ] response The response to cache.
|
|
432
|
+
#
|
|
433
|
+
# @return [ Boolean ] Whether the response should be cached.
|
|
434
|
+
def cacheable_response?(response)
|
|
435
|
+
!response.cached? &&
|
|
436
|
+
!streaming? &&
|
|
437
|
+
!response.mock? &&
|
|
438
|
+
(response.return_code.nil? || response.return_code == :ok)
|
|
439
|
+
end
|
|
440
|
+
|
|
182
441
|
# Checks if two hashes are equal or not, discarding
|
|
183
442
|
# first-level hash order.
|
|
184
443
|
#
|
|
@@ -199,11 +458,16 @@ module Typhoeus
|
|
|
199
458
|
def hashable_string_for(obj)
|
|
200
459
|
case obj
|
|
201
460
|
when Hash
|
|
202
|
-
|
|
461
|
+
pairs = obj.map do |key, value|
|
|
462
|
+
"#{hashable_string_for(key)}#{hashable_string_for(value)}"
|
|
463
|
+
end
|
|
464
|
+
"hash:#{obj.size}:#{pairs.sort.join}"
|
|
203
465
|
when Array
|
|
204
|
-
obj.map {|
|
|
466
|
+
values = obj.map { |value| hashable_string_for(value) }
|
|
467
|
+
"array:#{obj.size}:#{values.join}"
|
|
205
468
|
else
|
|
206
|
-
obj.to_s
|
|
469
|
+
value = obj.to_s
|
|
470
|
+
"#{obj.class}:#{value.bytesize}:#{value}"
|
|
207
471
|
end
|
|
208
472
|
end
|
|
209
473
|
|
|
@@ -211,13 +475,36 @@ module Typhoeus
|
|
|
211
475
|
def set_defaults
|
|
212
476
|
default_user_agent = Config.user_agent || Typhoeus::USER_AGENT
|
|
213
477
|
|
|
214
|
-
|
|
215
|
-
|
|
478
|
+
headers = (options[:headers] || {}).dup
|
|
479
|
+
unless header_key?(headers, "User-Agent")
|
|
480
|
+
headers = {"User-Agent" => default_user_agent}.merge(headers)
|
|
481
|
+
end
|
|
482
|
+
# An empty value suppresses libcurl's 100-continue. A nil value opts out
|
|
483
|
+
# of the header entirely, which lets libcurl add it when it sees fit.
|
|
484
|
+
unless header_key?(headers, "Expect")
|
|
485
|
+
headers["Expect"] = ""
|
|
486
|
+
end
|
|
487
|
+
headers.reject! { |key, value| key.to_s.casecmp?("Expect") && value.nil? }
|
|
488
|
+
options[:headers] = headers
|
|
216
489
|
options[:verbose] = Typhoeus::Config.verbose if options[:verbose].nil? && !Typhoeus::Config.verbose.nil?
|
|
217
490
|
options[:timeout] = Typhoeus::Config.timeout if options[:timeout].nil? && !Typhoeus::Config.timeout.nil?
|
|
218
491
|
options[:connecttimeout] = Typhoeus::Config.connecttimeout if options[:connecttimeout].nil? && !Typhoeus::Config.connecttimeout.nil?
|
|
219
492
|
options[:maxredirs] ||= 50
|
|
220
493
|
options[:proxy] = Typhoeus::Config.proxy unless options.has_key?(:proxy) || Typhoeus::Config.proxy.nil?
|
|
494
|
+
options[:cainfo] = ENV["SSL_CERT_FILE"] if options[:cainfo].nil? && ENV["SSL_CERT_FILE"]
|
|
495
|
+
options[:capath] = ENV["SSL_CERT_DIR"] if options[:capath].nil? && ENV["SSL_CERT_DIR"]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Check the headers for a name without relying on its casing. Header names
|
|
499
|
+
# are case-insensitive and libcurl sends every spelling it is given, so a
|
|
500
|
+
# second default header would be added to the caller's value.
|
|
501
|
+
#
|
|
502
|
+
# @param [ Hash ] headers The request headers.
|
|
503
|
+
# @param [ String ] name The header name to look for.
|
|
504
|
+
#
|
|
505
|
+
# @return [ Boolean ] Whether the header is already present.
|
|
506
|
+
def header_key?(headers, name)
|
|
507
|
+
headers.keys.any? { |key| key.to_s.casecmp?(name) }
|
|
221
508
|
end
|
|
222
509
|
end
|
|
223
510
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'delegate'
|
|
2
4
|
|
|
3
5
|
module Typhoeus
|
|
@@ -23,8 +25,173 @@ module Typhoeus
|
|
|
23
25
|
parse
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
def initialize_dup(other)
|
|
29
|
+
super
|
|
30
|
+
copy_delegate(other)
|
|
31
|
+
end
|
|
32
|
+
private :initialize_dup
|
|
33
|
+
|
|
34
|
+
def initialize_clone(other, freeze: nil)
|
|
35
|
+
if freeze.nil?
|
|
36
|
+
super(other)
|
|
37
|
+
else
|
|
38
|
+
super(other, freeze: freeze)
|
|
39
|
+
end
|
|
40
|
+
copy_delegate(other)
|
|
41
|
+
end
|
|
42
|
+
private :initialize_clone
|
|
43
|
+
|
|
26
44
|
def [](key)
|
|
27
|
-
|
|
45
|
+
@sanitized[convert_key(key)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def []=(key, value)
|
|
49
|
+
# Store string keys so the delegated Hash and the case-insensitive
|
|
50
|
+
# index describe the same header set even when a caller used a symbol.
|
|
51
|
+
key = key.to_s
|
|
52
|
+
delete_underlying_key(key)
|
|
53
|
+
__getobj__[key] = value
|
|
54
|
+
@sanitized[convert_key(key)] = value
|
|
55
|
+
end
|
|
56
|
+
alias_method :store, :[]=
|
|
57
|
+
|
|
58
|
+
def merge(other)
|
|
59
|
+
merged = dup
|
|
60
|
+
merged.update(other) do |key, old, new|
|
|
61
|
+
block_given? ? yield(key, old, new) : new
|
|
62
|
+
end
|
|
63
|
+
merged
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete(key)
|
|
67
|
+
value = self[key]
|
|
68
|
+
delete_underlying_key(key)
|
|
69
|
+
@sanitized.delete(convert_key(key))
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def clear
|
|
74
|
+
__getobj__.clear
|
|
75
|
+
@sanitized.clear
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update(other)
|
|
80
|
+
other.each_pair do |key, value|
|
|
81
|
+
value = yield(key, self[key], value) if block_given? && include?(key)
|
|
82
|
+
self[key] = value
|
|
83
|
+
end
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
alias_method :merge!, :update
|
|
87
|
+
|
|
88
|
+
def replace(other)
|
|
89
|
+
clear
|
|
90
|
+
update(other)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def delete_if(&block)
|
|
94
|
+
return enum_for(:delete_if) unless block
|
|
95
|
+
|
|
96
|
+
super
|
|
97
|
+
rebuild_sanitized
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def keep_if(&block)
|
|
102
|
+
return enum_for(:keep_if) unless block
|
|
103
|
+
|
|
104
|
+
super
|
|
105
|
+
rebuild_sanitized
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def select!(&block)
|
|
110
|
+
return enum_for(:select!) unless block
|
|
111
|
+
|
|
112
|
+
result = super
|
|
113
|
+
rebuild_sanitized
|
|
114
|
+
result ? self : nil
|
|
115
|
+
end
|
|
116
|
+
alias_method :filter!, :select!
|
|
117
|
+
|
|
118
|
+
def reject!(&block)
|
|
119
|
+
return enum_for(:reject!) unless block
|
|
120
|
+
|
|
121
|
+
result = super
|
|
122
|
+
rebuild_sanitized
|
|
123
|
+
result ? self : nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def shift
|
|
127
|
+
result = super
|
|
128
|
+
rebuild_sanitized
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def compact!
|
|
133
|
+
result = super
|
|
134
|
+
rebuild_sanitized
|
|
135
|
+
result ? self : nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def transform_keys!(*args, &block)
|
|
139
|
+
return enum_for(:transform_keys!, *args) if !block && args.empty?
|
|
140
|
+
|
|
141
|
+
super
|
|
142
|
+
rebuild_sanitized
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def transform_values!(&block)
|
|
147
|
+
return enum_for(:transform_values!) unless block
|
|
148
|
+
|
|
149
|
+
super
|
|
150
|
+
rebuild_sanitized
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def include?(key)
|
|
155
|
+
@sanitized.include?(convert_key(key))
|
|
156
|
+
end
|
|
157
|
+
alias_method :has_key?, :include?
|
|
158
|
+
alias_method :key?, :include?
|
|
159
|
+
alias_method :member?, :include?
|
|
160
|
+
|
|
161
|
+
def fetch(key, *extras, &block)
|
|
162
|
+
if block
|
|
163
|
+
# Hash#fetch yields the key the caller asked for, not a normalized
|
|
164
|
+
# one. Keep that behavior while looking up case-insensitively.
|
|
165
|
+
@sanitized.fetch(convert_key(key), *extras) { block.call(key) }
|
|
166
|
+
else
|
|
167
|
+
@sanitized.fetch(convert_key(key), *extras)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def dig(key, *identifiers)
|
|
172
|
+
@sanitized.dig(convert_key(key), *identifiers)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def assoc(key)
|
|
176
|
+
@sanitized.assoc(convert_key(key))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def values_at(*keys)
|
|
180
|
+
keys.map! { |key| convert_key(key) }
|
|
181
|
+
@sanitized.values_at(*keys)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fetch_values(*indices, &block)
|
|
185
|
+
indices.map do |key|
|
|
186
|
+
converted = convert_key(key)
|
|
187
|
+
if @sanitized.key?(converted)
|
|
188
|
+
@sanitized[converted]
|
|
189
|
+
elsif block
|
|
190
|
+
block.call(key)
|
|
191
|
+
else
|
|
192
|
+
@sanitized.fetch(converted)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
28
195
|
end
|
|
29
196
|
|
|
30
197
|
# Parses the raw header.
|
|
@@ -32,12 +199,11 @@ module Typhoeus
|
|
|
32
199
|
# @example Parse header.
|
|
33
200
|
# header.parse
|
|
34
201
|
def parse
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
raw.each do |k, v|
|
|
202
|
+
if raw.respond_to?(:each_pair)
|
|
203
|
+
raw.each_pair do |k, v|
|
|
38
204
|
process_pair(k, v)
|
|
39
205
|
end
|
|
40
|
-
|
|
206
|
+
elsif raw.is_a?(String)
|
|
41
207
|
raw.split(/\r?\n(?!\s)/).each do |header|
|
|
42
208
|
header.strip!
|
|
43
209
|
next if header.empty? || header.start_with?( 'HTTP/' )
|
|
@@ -60,8 +226,9 @@ module Typhoeus
|
|
|
60
226
|
#
|
|
61
227
|
# @return [ void ]
|
|
62
228
|
def process_pair(key, value)
|
|
63
|
-
|
|
64
|
-
@sanitized
|
|
229
|
+
sanitized_key = convert_key(key)
|
|
230
|
+
set_value(sanitized_key, duplicate_value(value), @sanitized)
|
|
231
|
+
self[key] = @sanitized[sanitized_key]
|
|
65
232
|
end
|
|
66
233
|
|
|
67
234
|
# Sets value for key in specified hash
|
|
@@ -90,14 +257,67 @@ module Typhoeus
|
|
|
90
257
|
@raw || ''
|
|
91
258
|
end
|
|
92
259
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
260
|
+
def convert_key(key)
|
|
261
|
+
key.to_s.downcase
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def duplicate_value(value)
|
|
265
|
+
case value
|
|
266
|
+
when Array
|
|
267
|
+
value.map { |nested| duplicate_value(nested) }
|
|
268
|
+
when String
|
|
269
|
+
value.dup
|
|
99
270
|
else
|
|
100
|
-
|
|
271
|
+
value
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# DelegateClass copies only the delegated hash itself. Rebuild both
|
|
276
|
+
# hashes so later mutations cannot reach the original header. The
|
|
277
|
+
# sanitized index is built from the copied hash so both stores keep
|
|
278
|
+
# referring to the same values, as they do on the original.
|
|
279
|
+
def copy_delegate(other)
|
|
280
|
+
copy = {}
|
|
281
|
+
other.__getobj__.each do |key, value|
|
|
282
|
+
copy[key] = duplicate_value(value)
|
|
283
|
+
end
|
|
284
|
+
__setobj__(copy)
|
|
285
|
+
|
|
286
|
+
@sanitized = {}
|
|
287
|
+
copy.each do |key, value|
|
|
288
|
+
converted = convert_key(key)
|
|
289
|
+
if @sanitized.key?(converted)
|
|
290
|
+
existing = @sanitized[converted]
|
|
291
|
+
existing = [existing] unless existing.is_a?(Array)
|
|
292
|
+
existing.concat(value.is_a?(Array) ? value : [value])
|
|
293
|
+
@sanitized[converted] = existing
|
|
294
|
+
else
|
|
295
|
+
@sanitized[converted] = value
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def delete_underlying_key(key)
|
|
301
|
+
converted = convert_key(key)
|
|
302
|
+
__getobj__.keys.each do |existing_key|
|
|
303
|
+
__getobj__.delete(existing_key) if convert_key(existing_key) == converted
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Rebuild the case-insensitive index after a Hash mutator that cannot be
|
|
308
|
+
# expressed through #[]=.
|
|
309
|
+
def rebuild_sanitized
|
|
310
|
+
@sanitized = {}
|
|
311
|
+
__getobj__.each do |key, value|
|
|
312
|
+
converted = convert_key(key)
|
|
313
|
+
if @sanitized.key?(converted)
|
|
314
|
+
existing = @sanitized[converted]
|
|
315
|
+
existing = [existing] unless existing.is_a?(Array)
|
|
316
|
+
existing.concat(value.is_a?(Array) ? value : [value])
|
|
317
|
+
@sanitized[converted] = existing
|
|
318
|
+
else
|
|
319
|
+
@sanitized[converted] = value
|
|
320
|
+
end
|
|
101
321
|
end
|
|
102
322
|
end
|
|
103
323
|
end
|