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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/capture_warning.rb +10 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/test/support/fakeio.rb +21 -0
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +218 -0
data/lib/http/headers.rb
ADDED
|
@@ -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
|