rest-man 1.0.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 +7 -0
- data/.github/workflows/multi-matrix-test.yml +35 -0
- data/.github/workflows/single-matrix-test.yml +27 -0
- data/.gitignore +13 -0
- data/.mailmap +10 -0
- data/.rspec +2 -0
- data/.rubocop +2 -0
- data/.rubocop-disables.yml +386 -0
- data/.rubocop.yml +8 -0
- data/AUTHORS +106 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +843 -0
- data/Rakefile +140 -0
- data/exe/restman +92 -0
- data/lib/rest-man.rb +2 -0
- data/lib/rest_man.rb +2 -0
- data/lib/restman/abstract_response.rb +252 -0
- data/lib/restman/exceptions.rb +238 -0
- data/lib/restman/params_array.rb +72 -0
- data/lib/restman/payload.rb +234 -0
- data/lib/restman/platform.rb +49 -0
- data/lib/restman/raw_response.rb +49 -0
- data/lib/restman/request.rb +859 -0
- data/lib/restman/resource.rb +178 -0
- data/lib/restman/response.rb +90 -0
- data/lib/restman/utils.rb +274 -0
- data/lib/restman/version.rb +8 -0
- data/lib/restman/windows/root_certs.rb +105 -0
- data/lib/restman/windows.rb +8 -0
- data/lib/restman.rb +183 -0
- data/matrixeval.yml +73 -0
- data/rest-man.gemspec +41 -0
- data/spec/ISS.jpg +0 -0
- data/spec/cassettes/request_httpbin_with_basic_auth.yml +83 -0
- data/spec/cassettes/request_httpbin_with_cookies.yml +49 -0
- data/spec/cassettes/request_httpbin_with_cookies_2.yml +94 -0
- data/spec/cassettes/request_httpbin_with_cookies_3.yml +49 -0
- data/spec/cassettes/request_httpbin_with_encoding_deflate.yml +45 -0
- data/spec/cassettes/request_httpbin_with_encoding_deflate_and_accept_headers.yml +44 -0
- data/spec/cassettes/request_httpbin_with_encoding_gzip.yml +45 -0
- data/spec/cassettes/request_httpbin_with_encoding_gzip_and_accept_headers.yml +44 -0
- data/spec/cassettes/request_httpbin_with_user_agent.yml +44 -0
- data/spec/cassettes/request_mozilla_org.yml +151 -0
- data/spec/cassettes/request_mozilla_org_callback_returns_true.yml +178 -0
- data/spec/cassettes/request_mozilla_org_with_system_cert.yml +152 -0
- data/spec/cassettes/request_mozilla_org_with_system_cert_and_callback.yml +151 -0
- data/spec/helpers.rb +54 -0
- data/spec/integration/_lib.rb +1 -0
- data/spec/integration/capath_digicert/README +8 -0
- data/spec/integration/capath_digicert/ce5e74ef.0 +1 -0
- data/spec/integration/capath_digicert/digicert.crt +20 -0
- data/spec/integration/capath_digicert/update +1 -0
- data/spec/integration/capath_verisign/415660c1.0 +14 -0
- data/spec/integration/capath_verisign/7651b327.0 +14 -0
- data/spec/integration/capath_verisign/README +8 -0
- data/spec/integration/capath_verisign/verisign.crt +14 -0
- data/spec/integration/certs/digicert.crt +20 -0
- data/spec/integration/certs/verisign.crt +14 -0
- data/spec/integration/httpbin_spec.rb +137 -0
- data/spec/integration/integration_spec.rb +118 -0
- data/spec/integration/request_spec.rb +134 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/unit/_lib.rb +1 -0
- data/spec/unit/abstract_response_spec.rb +145 -0
- data/spec/unit/exceptions_spec.rb +108 -0
- data/spec/unit/params_array_spec.rb +36 -0
- data/spec/unit/payload_spec.rb +295 -0
- data/spec/unit/raw_response_spec.rb +22 -0
- data/spec/unit/request2_spec.rb +54 -0
- data/spec/unit/request_spec.rb +1205 -0
- data/spec/unit/resource_spec.rb +134 -0
- data/spec/unit/response_spec.rb +252 -0
- data/spec/unit/restclient_spec.rb +80 -0
- data/spec/unit/utils_spec.rb +147 -0
- data/spec/unit/windows/root_certs_spec.rb +22 -0
- metadata +336 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
module RestMan
|
2
|
+
|
3
|
+
# Hash of HTTP status code => message.
|
4
|
+
#
|
5
|
+
# 1xx: Informational - Request received, continuing process
|
6
|
+
# 2xx: Success - The action was successfully received, understood, and
|
7
|
+
# accepted
|
8
|
+
# 3xx: Redirection - Further action must be taken in order to complete the
|
9
|
+
# request
|
10
|
+
# 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
|
11
|
+
# 5xx: Server Error - The server failed to fulfill an apparently valid
|
12
|
+
# request
|
13
|
+
#
|
14
|
+
# @see
|
15
|
+
# http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
16
|
+
#
|
17
|
+
STATUSES = {100 => 'Continue',
|
18
|
+
101 => 'Switching Protocols',
|
19
|
+
102 => 'Processing', #WebDAV
|
20
|
+
|
21
|
+
200 => 'OK',
|
22
|
+
201 => 'Created',
|
23
|
+
202 => 'Accepted',
|
24
|
+
203 => 'Non-Authoritative Information', # http/1.1
|
25
|
+
204 => 'No Content',
|
26
|
+
205 => 'Reset Content',
|
27
|
+
206 => 'Partial Content',
|
28
|
+
207 => 'Multi-Status', #WebDAV
|
29
|
+
208 => 'Already Reported', # RFC5842
|
30
|
+
226 => 'IM Used', # RFC3229
|
31
|
+
|
32
|
+
300 => 'Multiple Choices',
|
33
|
+
301 => 'Moved Permanently',
|
34
|
+
302 => 'Found',
|
35
|
+
303 => 'See Other', # http/1.1
|
36
|
+
304 => 'Not Modified',
|
37
|
+
305 => 'Use Proxy', # http/1.1
|
38
|
+
306 => 'Switch Proxy', # no longer used
|
39
|
+
307 => 'Temporary Redirect', # http/1.1
|
40
|
+
308 => 'Permanent Redirect', # RFC7538
|
41
|
+
|
42
|
+
400 => 'Bad Request',
|
43
|
+
401 => 'Unauthorized',
|
44
|
+
402 => 'Payment Required',
|
45
|
+
403 => 'Forbidden',
|
46
|
+
404 => 'Not Found',
|
47
|
+
405 => 'Method Not Allowed',
|
48
|
+
406 => 'Not Acceptable',
|
49
|
+
407 => 'Proxy Authentication Required',
|
50
|
+
408 => 'Request Timeout',
|
51
|
+
409 => 'Conflict',
|
52
|
+
410 => 'Gone',
|
53
|
+
411 => 'Length Required',
|
54
|
+
412 => 'Precondition Failed',
|
55
|
+
413 => 'Payload Too Large', # RFC7231 (renamed, see below)
|
56
|
+
414 => 'URI Too Long', # RFC7231 (renamed, see below)
|
57
|
+
415 => 'Unsupported Media Type',
|
58
|
+
416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
|
59
|
+
417 => 'Expectation Failed',
|
60
|
+
418 => 'I\'m A Teapot', #RFC2324
|
61
|
+
421 => 'Too Many Connections From This IP',
|
62
|
+
422 => 'Unprocessable Entity', #WebDAV
|
63
|
+
423 => 'Locked', #WebDAV
|
64
|
+
424 => 'Failed Dependency', #WebDAV
|
65
|
+
425 => 'Unordered Collection', #WebDAV
|
66
|
+
426 => 'Upgrade Required',
|
67
|
+
428 => 'Precondition Required', #RFC6585
|
68
|
+
429 => 'Too Many Requests', #RFC6585
|
69
|
+
431 => 'Request Header Fields Too Large', #RFC6585
|
70
|
+
449 => 'Retry With', #Microsoft
|
71
|
+
450 => 'Blocked By Windows Parental Controls', #Microsoft
|
72
|
+
|
73
|
+
500 => 'Internal Server Error',
|
74
|
+
501 => 'Not Implemented',
|
75
|
+
502 => 'Bad Gateway',
|
76
|
+
503 => 'Service Unavailable',
|
77
|
+
504 => 'Gateway Timeout',
|
78
|
+
505 => 'HTTP Version Not Supported',
|
79
|
+
506 => 'Variant Also Negotiates',
|
80
|
+
507 => 'Insufficient Storage', #WebDAV
|
81
|
+
508 => 'Loop Detected', # RFC5842
|
82
|
+
509 => 'Bandwidth Limit Exceeded', #Apache
|
83
|
+
510 => 'Not Extended',
|
84
|
+
511 => 'Network Authentication Required', # RFC6585
|
85
|
+
}
|
86
|
+
|
87
|
+
STATUSES_COMPATIBILITY = {
|
88
|
+
# The RFCs all specify "Not Found", but "Resource Not Found" was used in
|
89
|
+
# earlier RestMan releases.
|
90
|
+
404 => ['ResourceNotFound'],
|
91
|
+
|
92
|
+
# HTTP 413 was renamed to "Payload Too Large" in RFC7231.
|
93
|
+
413 => ['RequestEntityTooLarge'],
|
94
|
+
|
95
|
+
# HTTP 414 was renamed to "URI Too Long" in RFC7231.
|
96
|
+
414 => ['RequestURITooLong'],
|
97
|
+
|
98
|
+
# HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233.
|
99
|
+
416 => ['RequestedRangeNotSatisfiable'],
|
100
|
+
}
|
101
|
+
|
102
|
+
|
103
|
+
# This is the base RestMan exception class. Rescue it if you want to
|
104
|
+
# catch any exception that your request might raise
|
105
|
+
# You can get the status code by e.http_code, or see anything about the
|
106
|
+
# response via e.response.
|
107
|
+
# For example, the entire result body (which is
|
108
|
+
# probably an HTML error page) is e.response.
|
109
|
+
class Exception < RuntimeError
|
110
|
+
attr_accessor :response
|
111
|
+
attr_accessor :original_exception
|
112
|
+
attr_writer :message
|
113
|
+
|
114
|
+
def initialize response = nil, initial_response_code = nil
|
115
|
+
@response = response
|
116
|
+
@message = nil
|
117
|
+
@initial_response_code = initial_response_code
|
118
|
+
end
|
119
|
+
|
120
|
+
def http_code
|
121
|
+
# return integer for compatibility
|
122
|
+
if @response
|
123
|
+
@response.code.to_i
|
124
|
+
else
|
125
|
+
@initial_response_code
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def http_headers
|
130
|
+
@response.headers if @response
|
131
|
+
end
|
132
|
+
|
133
|
+
def http_body
|
134
|
+
@response.body if @response
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
message
|
139
|
+
end
|
140
|
+
|
141
|
+
def message
|
142
|
+
@message || default_message
|
143
|
+
end
|
144
|
+
|
145
|
+
def default_message
|
146
|
+
self.class.name
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Compatibility
|
151
|
+
class ExceptionWithResponse < RestMan::Exception
|
152
|
+
end
|
153
|
+
|
154
|
+
# The request failed with an error code not managed by the code
|
155
|
+
class RequestFailed < ExceptionWithResponse
|
156
|
+
|
157
|
+
def default_message
|
158
|
+
"HTTP status code #{http_code}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_s
|
162
|
+
message
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# RestMan exception classes. TODO: move all exceptions into this module.
|
167
|
+
#
|
168
|
+
# We will a create an exception for each status code, see
|
169
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
170
|
+
#
|
171
|
+
module Exceptions
|
172
|
+
# Map http status codes to the corresponding exception class
|
173
|
+
EXCEPTIONS_MAP = {}
|
174
|
+
end
|
175
|
+
|
176
|
+
# Create HTTP status exception classes
|
177
|
+
STATUSES.each_pair do |code, message|
|
178
|
+
klass = Class.new(RequestFailed) do
|
179
|
+
send(:define_method, :default_message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
|
180
|
+
end
|
181
|
+
klass_constant = const_set(message.delete(' \-\''), klass)
|
182
|
+
Exceptions::EXCEPTIONS_MAP[code] = klass_constant
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create HTTP status exception classes used for backwards compatibility
|
186
|
+
STATUSES_COMPATIBILITY.each_pair do |code, compat_list|
|
187
|
+
klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
|
188
|
+
compat_list.each do |old_name|
|
189
|
+
const_set(old_name, klass)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
module Exceptions
|
194
|
+
# We have to split the Exceptions module like we do here because the
|
195
|
+
# EXCEPTIONS_MAP is under Exceptions, but we depend on
|
196
|
+
# RestMan::RequestTimeout below.
|
197
|
+
|
198
|
+
# Base class for request timeouts.
|
199
|
+
#
|
200
|
+
# NB: Previous releases of rest-man would raise RequestTimeout both for
|
201
|
+
# HTTP 408 responses and for actual connection timeouts.
|
202
|
+
class Timeout < RestMan::RequestTimeout
|
203
|
+
def initialize(message=nil, original_exception=nil)
|
204
|
+
super(nil, nil)
|
205
|
+
self.message = message if message
|
206
|
+
self.original_exception = original_exception if original_exception
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Timeout when connecting to a server. Typically wraps Net::OpenTimeout (in
|
211
|
+
# ruby 2.0 or greater).
|
212
|
+
class OpenTimeout < Timeout
|
213
|
+
def default_message
|
214
|
+
'Timed out connecting to server'
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Timeout when reading from a server. Typically wraps Net::ReadTimeout (in
|
219
|
+
# ruby 2.0 or greater).
|
220
|
+
class ReadTimeout < Timeout
|
221
|
+
def default_message
|
222
|
+
'Timed out reading data from server'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# The server broke the connection prior to the request completing. Usually
|
229
|
+
# this means it crashed, or sometimes that your network connection was
|
230
|
+
# severed before it could complete.
|
231
|
+
class ServerBrokeConnection < RestMan::Exception
|
232
|
+
def initialize(message = 'Server broke connection')
|
233
|
+
super nil, nil
|
234
|
+
self.message = message
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RestMan
|
2
|
+
|
3
|
+
# The ParamsArray class is used to represent an ordered list of [key, value]
|
4
|
+
# pairs. Use this when you need to include a key multiple times or want
|
5
|
+
# explicit control over parameter ordering.
|
6
|
+
#
|
7
|
+
# Most of the request payload & parameter functions normally accept a Hash of
|
8
|
+
# keys => values, which does not allow for duplicated keys.
|
9
|
+
#
|
10
|
+
# @see RestMan::Utils.encode_query_string
|
11
|
+
# @see RestMan::Utils.flatten_params
|
12
|
+
#
|
13
|
+
class ParamsArray
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
# @param array [Array<Array>] An array of parameter key,value pairs. These
|
17
|
+
# pairs may be 2 element arrays [key, value] or single element hashes
|
18
|
+
# {key => value}. They may also be single element arrays to represent a
|
19
|
+
# key with no value.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
|
23
|
+
# This will be encoded as "foo=123&foo=456&bar=789"
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# >> ParamsArray.new({foo: 123, bar: 456})
|
27
|
+
# This is valid, but there's no reason not to just use the Hash directly
|
28
|
+
# instead of a ParamsArray.
|
29
|
+
#
|
30
|
+
#
|
31
|
+
def initialize(array)
|
32
|
+
@array = process_input(array)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(*args, &blk)
|
36
|
+
@array.each(*args, &blk)
|
37
|
+
end
|
38
|
+
|
39
|
+
def empty?
|
40
|
+
@array.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def process_input(array)
|
46
|
+
array.map {|v| process_pair(v) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# A pair may be:
|
50
|
+
# - A single element hash, e.g. {foo: 'bar'}
|
51
|
+
# - A two element array, e.g. ['foo', 'bar']
|
52
|
+
# - A one element array, e.g. ['foo']
|
53
|
+
#
|
54
|
+
def process_pair(pair)
|
55
|
+
case pair
|
56
|
+
when Hash
|
57
|
+
if pair.length != 1
|
58
|
+
raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
|
59
|
+
end
|
60
|
+
pair.to_a.fetch(0)
|
61
|
+
when Array
|
62
|
+
if pair.length > 2
|
63
|
+
raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
|
64
|
+
end
|
65
|
+
[pair.fetch(0), pair[1]]
|
66
|
+
else
|
67
|
+
# recurse, converting any non-array to an array
|
68
|
+
process_pair(pair.to_a)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
begin
|
6
|
+
# Use mime/types/columnar if available, for reduced memory usage
|
7
|
+
require 'mime/types/columnar'
|
8
|
+
rescue LoadError
|
9
|
+
require 'mime/types'
|
10
|
+
end
|
11
|
+
|
12
|
+
module RestMan
|
13
|
+
module Payload
|
14
|
+
extend self
|
15
|
+
|
16
|
+
def generate(params)
|
17
|
+
if params.is_a?(RestMan::Payload::Base)
|
18
|
+
# pass through Payload objects unchanged
|
19
|
+
params
|
20
|
+
elsif params.is_a?(String)
|
21
|
+
Base.new(params)
|
22
|
+
elsif params.is_a?(Hash)
|
23
|
+
if params.delete(:multipart) == true || has_file?(params)
|
24
|
+
Multipart.new(params)
|
25
|
+
else
|
26
|
+
UrlEncoded.new(params)
|
27
|
+
end
|
28
|
+
elsif params.is_a?(ParamsArray)
|
29
|
+
if _has_file?(params)
|
30
|
+
Multipart.new(params)
|
31
|
+
else
|
32
|
+
UrlEncoded.new(params)
|
33
|
+
end
|
34
|
+
elsif params.respond_to?(:read)
|
35
|
+
Streamed.new(params)
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_file?(params)
|
42
|
+
unless params.is_a?(Hash)
|
43
|
+
raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
|
44
|
+
end
|
45
|
+
_has_file?(params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def _has_file?(obj)
|
49
|
+
case obj
|
50
|
+
when Hash, ParamsArray
|
51
|
+
obj.any? {|_, v| _has_file?(v) }
|
52
|
+
when Array
|
53
|
+
obj.any? {|v| _has_file?(v) }
|
54
|
+
else
|
55
|
+
obj.respond_to?(:path) && obj.respond_to?(:read)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Base
|
60
|
+
def initialize(params)
|
61
|
+
build_stream(params)
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_stream(params)
|
65
|
+
@stream = StringIO.new(params)
|
66
|
+
@stream.seek(0)
|
67
|
+
end
|
68
|
+
|
69
|
+
def read(*args)
|
70
|
+
@stream.read(*args)
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
result = read
|
75
|
+
@stream.seek(0)
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
def headers
|
80
|
+
{'Content-Length' => size.to_s}
|
81
|
+
end
|
82
|
+
|
83
|
+
def size
|
84
|
+
@stream.size
|
85
|
+
end
|
86
|
+
|
87
|
+
alias :length :size
|
88
|
+
|
89
|
+
def close
|
90
|
+
@stream.close unless @stream.closed?
|
91
|
+
end
|
92
|
+
|
93
|
+
def closed?
|
94
|
+
@stream.closed?
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s_inspect
|
98
|
+
to_s.inspect
|
99
|
+
end
|
100
|
+
|
101
|
+
def short_inspect
|
102
|
+
if size && size > 500
|
103
|
+
"#{size} byte(s) length"
|
104
|
+
else
|
105
|
+
to_s_inspect
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
class Streamed < Base
|
112
|
+
def build_stream(params = nil)
|
113
|
+
@stream = params
|
114
|
+
end
|
115
|
+
|
116
|
+
def size
|
117
|
+
if @stream.respond_to?(:size)
|
118
|
+
@stream.size
|
119
|
+
elsif @stream.is_a?(IO)
|
120
|
+
@stream.stat.size
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# TODO (breaks compatibility): ought to use mime_for() to autodetect the
|
125
|
+
# Content-Type for stream objects that have a filename.
|
126
|
+
|
127
|
+
alias :length :size
|
128
|
+
end
|
129
|
+
|
130
|
+
class UrlEncoded < Base
|
131
|
+
def build_stream(params = nil)
|
132
|
+
@stream = StringIO.new(Utils.encode_query_string(params))
|
133
|
+
@stream.seek(0)
|
134
|
+
end
|
135
|
+
|
136
|
+
def headers
|
137
|
+
super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Multipart < Base
|
142
|
+
EOL = "\r\n"
|
143
|
+
|
144
|
+
def build_stream(params)
|
145
|
+
b = '--' + boundary
|
146
|
+
|
147
|
+
@stream = Tempfile.new('rest-man.multipart.')
|
148
|
+
@stream.binmode
|
149
|
+
@stream.write(b + EOL)
|
150
|
+
|
151
|
+
case params
|
152
|
+
when Hash, ParamsArray
|
153
|
+
x = Utils.flatten_params(params)
|
154
|
+
else
|
155
|
+
x = params
|
156
|
+
end
|
157
|
+
|
158
|
+
last_index = x.length - 1
|
159
|
+
x.each_with_index do |a, index|
|
160
|
+
k, v = * a
|
161
|
+
if v.respond_to?(:read) && v.respond_to?(:path)
|
162
|
+
create_file_field(@stream, k, v)
|
163
|
+
else
|
164
|
+
create_regular_field(@stream, k, v)
|
165
|
+
end
|
166
|
+
@stream.write(EOL + b)
|
167
|
+
@stream.write(EOL) unless last_index == index
|
168
|
+
end
|
169
|
+
@stream.write('--')
|
170
|
+
@stream.write(EOL)
|
171
|
+
@stream.seek(0)
|
172
|
+
end
|
173
|
+
|
174
|
+
def create_regular_field(s, k, v)
|
175
|
+
s.write("Content-Disposition: form-data; name=\"#{k}\"")
|
176
|
+
s.write(EOL)
|
177
|
+
s.write(EOL)
|
178
|
+
s.write(v)
|
179
|
+
end
|
180
|
+
|
181
|
+
def create_file_field(s, k, v)
|
182
|
+
begin
|
183
|
+
s.write("Content-Disposition: form-data;")
|
184
|
+
s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
|
185
|
+
s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
|
186
|
+
s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
|
187
|
+
s.write(EOL)
|
188
|
+
while (data = v.read(8124))
|
189
|
+
s.write(data)
|
190
|
+
end
|
191
|
+
ensure
|
192
|
+
v.close if v.respond_to?(:close)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def mime_for(path)
|
197
|
+
mime = MIME::Types.type_for path
|
198
|
+
mime.empty? ? 'text/plain' : mime[0].content_type
|
199
|
+
end
|
200
|
+
|
201
|
+
def boundary
|
202
|
+
return @boundary if defined?(@boundary) && @boundary
|
203
|
+
|
204
|
+
# Use the same algorithm used by WebKit: generate 16 random
|
205
|
+
# alphanumeric characters, replacing `+` `/` with `A` `B` (included in
|
206
|
+
# the list twice) to round out the set of 64.
|
207
|
+
s = SecureRandom.base64(12)
|
208
|
+
s.tr!('+/', 'AB')
|
209
|
+
|
210
|
+
@boundary = '----RubyFormBoundary' + s
|
211
|
+
end
|
212
|
+
|
213
|
+
# for Multipart do not escape the keys
|
214
|
+
#
|
215
|
+
# Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
|
216
|
+
# practice no major browser that I'm aware of uses percent encoding.
|
217
|
+
#
|
218
|
+
# Further discussion of multipart encoding:
|
219
|
+
# https://github.com/rest-man/rest-man/pull/403#issuecomment-156976930
|
220
|
+
#
|
221
|
+
def handle_key key
|
222
|
+
key
|
223
|
+
end
|
224
|
+
|
225
|
+
def headers
|
226
|
+
super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
|
227
|
+
end
|
228
|
+
|
229
|
+
def close
|
230
|
+
@stream.close!
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
|
3
|
+
module RestMan
|
4
|
+
module Platform
|
5
|
+
# Return true if we are running on a darwin-based Ruby platform. This will
|
6
|
+
# be false for jruby even on OS X.
|
7
|
+
#
|
8
|
+
# @return [Boolean]
|
9
|
+
def self.mac_mri?
|
10
|
+
RUBY_PLATFORM.include?('darwin')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return true if we are running on Windows.
|
14
|
+
#
|
15
|
+
# @return [Boolean]
|
16
|
+
#
|
17
|
+
def self.windows?
|
18
|
+
# Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard
|
19
|
+
# library uses that to test what platform it's on.
|
20
|
+
!!File::ALT_SEPARATOR
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return true if we are running on jruby.
|
24
|
+
#
|
25
|
+
# @return [Boolean]
|
26
|
+
#
|
27
|
+
def self.jruby?
|
28
|
+
# defined on mri >= 1.9
|
29
|
+
RUBY_ENGINE == 'jruby'
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.architecture
|
33
|
+
"#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.ruby_agent_version
|
37
|
+
case RUBY_ENGINE
|
38
|
+
when 'jruby'
|
39
|
+
"jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})"
|
40
|
+
else
|
41
|
+
"#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.default_user_agent
|
46
|
+
"rest-man/#{VERSION} (#{architecture}) #{ruby_agent_version}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module RestMan
|
2
|
+
# The response from RestMan on a raw request looks like a string, but is
|
3
|
+
# actually one of these. 99% of the time you're making a rest call all you
|
4
|
+
# care about is the body, but on the occasion you want to fetch the
|
5
|
+
# headers you can:
|
6
|
+
#
|
7
|
+
# RestMan.get('http://example.com').headers[:content_type]
|
8
|
+
#
|
9
|
+
# In addition, if you do not use the response as a string, you can access
|
10
|
+
# a Tempfile object at res.file, which contains the path to the raw
|
11
|
+
# downloaded request body.
|
12
|
+
class RawResponse
|
13
|
+
|
14
|
+
include AbstractResponse
|
15
|
+
|
16
|
+
attr_reader :file, :request, :start_time, :end_time
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"<RestMan::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Tempfile] tempfile The temporary file containing the body
|
23
|
+
# @param [Net::HTTPResponse] net_http_res
|
24
|
+
# @param [RestMan::Request] request
|
25
|
+
# @param [Time] start_time
|
26
|
+
def initialize(tempfile, net_http_res, request, start_time=nil)
|
27
|
+
@file = tempfile
|
28
|
+
|
29
|
+
# reopen the tempfile so we can read it
|
30
|
+
@file.open
|
31
|
+
|
32
|
+
response_set_vars(net_http_res, request, start_time)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
body
|
37
|
+
end
|
38
|
+
|
39
|
+
def body
|
40
|
+
@file.rewind
|
41
|
+
@file.read
|
42
|
+
end
|
43
|
+
|
44
|
+
def size
|
45
|
+
file.size
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|