rest-client 2.0.0.rc2-x86-mswin32 → 2.0.0.rc3-x86-mswin32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop-disables.yml +17 -8
- data/.travis.yml +32 -2
- data/AUTHORS +6 -0
- data/README.md +578 -0
- data/Rakefile +1 -1
- data/history.md +45 -13
- data/lib/restclient.rb +4 -2
- data/lib/restclient/abstract_response.rb +51 -25
- data/lib/restclient/exceptions.rb +45 -6
- data/lib/restclient/params_array.rb +72 -0
- data/lib/restclient/payload.rb +40 -69
- data/lib/restclient/raw_response.rb +1 -2
- data/lib/restclient/request.rb +372 -199
- data/lib/restclient/response.rb +11 -8
- data/lib/restclient/utils.rb +144 -2
- data/lib/restclient/version.rb +1 -1
- data/rest-client.gemspec +5 -5
- data/spec/helpers.rb +8 -0
- data/spec/integration/httpbin_spec.rb +7 -7
- data/spec/integration/integration_spec.rb +34 -24
- data/spec/integration/request_spec.rb +1 -1
- data/spec/spec_helper.rb +8 -1
- data/spec/unit/abstract_response_spec.rb +76 -33
- data/spec/unit/exceptions_spec.rb +27 -21
- data/spec/unit/params_array_spec.rb +36 -0
- data/spec/unit/payload_spec.rb +71 -53
- data/spec/unit/raw_response_spec.rb +3 -3
- data/spec/unit/request2_spec.rb +29 -7
- data/spec/unit/request_spec.rb +552 -415
- data/spec/unit/resource_spec.rb +25 -25
- data/spec/unit/response_spec.rb +86 -64
- data/spec/unit/restclient_spec.rb +13 -13
- data/spec/unit/utils_spec.rb +117 -41
- data/spec/unit/windows/root_certs_spec.rb +2 -2
- metadata +15 -12
- data/README.rdoc +0 -410
data/Rakefile
CHANGED
@@ -127,6 +127,6 @@ Rake::RDocTask.new do |t|
|
|
127
127
|
t.title = "rest-client, fetch RESTful resources effortlessly"
|
128
128
|
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
129
129
|
t.options << '--charset' << 'utf-8'
|
130
|
-
t.rdoc_files.include('README.
|
130
|
+
t.rdoc_files.include('README.md')
|
131
131
|
t.rdoc_files.include('lib/*.rb')
|
132
132
|
end
|
data/history.md
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
This release is largely API compatible, but makes several breaking changes.
|
4
4
|
|
5
|
-
- Drop support for Ruby 1.9
|
5
|
+
- Drop support for Ruby 1.9
|
6
|
+
- Allow mime-types as new as 3.x (requires ruby 2.0)
|
6
7
|
- Respect Content-Type charset header provided by server. Previously,
|
7
8
|
rest-client would not override the string encoding chosen by Net::HTTP. Now
|
8
9
|
responses that specify a charset will yield a body string in that encoding.
|
@@ -20,8 +21,8 @@ This release is largely API compatible, but makes several breaking changes.
|
|
20
21
|
inherits from `ExceptionWithResponse`. Previously, HTTP 304, 401, and 404
|
21
22
|
inherited directly from `ExceptionWithResponse` rather than from
|
22
23
|
`RequestFailed`. Now _all_ HTTP status code exceptions inherit from both.
|
23
|
-
- Rename `:timeout` to `:read_timeout`. When `:timeout` is
|
24
|
-
`:read_timeout` and `:open_timeout`.
|
24
|
+
- Rename the `:timeout` request option to `:read_timeout`. When `:timeout` is
|
25
|
+
passed, now set both `:read_timeout` and `:open_timeout`.
|
25
26
|
- Change default HTTP Accept header to `*/*`
|
26
27
|
- Use a more descriptive User-Agent header by default
|
27
28
|
- Drop RC4-MD5 from default cipher list
|
@@ -37,12 +38,30 @@ This release is largely API compatible, but makes several breaking changes.
|
|
37
38
|
- `Response#to_i` will now behave like `String#to_i` instead of returning the
|
38
39
|
HTTP response code, which was very surprising behavior.
|
39
40
|
- `Response#body` and `#to_s` will now return a true `String` object rather
|
40
|
-
than self. Previously there was no easy way to get the true `String`
|
41
|
-
instead of the Frankenstein response string object with
|
42
|
-
mixed in.
|
41
|
+
than self. Previously there was no easy way to get the true `String`
|
42
|
+
response instead of the Frankenstein response string object with
|
43
|
+
AbstractResponse mixed in.
|
44
|
+
- Response objects no longer accept an extra request args hash, but instead
|
45
|
+
access request args directly from the request object, which reduces
|
46
|
+
confusion and duplication.
|
43
47
|
- Handle multiple HTTP response headers with the same name (except for
|
44
48
|
Set-Cookie, which is special) by joining the values with a comma space,
|
45
49
|
compliant with RFC 7230
|
50
|
+
- Rewrite cookie support to be much smarter and to use cookie jars consistently
|
51
|
+
for requests, responses, and redirection (resolving long-standing complaints
|
52
|
+
about the previously broken behavior):
|
53
|
+
- The `:cookies` option may now be a Hash of Strings, an Array of
|
54
|
+
HTTP::Cookie objects, or a full HTTP::CookieJar.
|
55
|
+
- Add `RestClient::Request#cookie_jar` and reimplement `Request#cookies` to
|
56
|
+
be a wrapper around the cookie jar.
|
57
|
+
- Still support passing the `:cookies` option in the headers hash, but now
|
58
|
+
raise ArgumentError if that option is also passed to `Request#initialize`.
|
59
|
+
- Warn if both `:cookies` and a `Cookie` header are supplied.
|
60
|
+
- Use the `Request#cookie_jar` as the basis for `Response#cookie_jar`,
|
61
|
+
creating a copy of the jar and adding any newly received cookies.
|
62
|
+
- When following redirection, also use this same strategy so that cookies
|
63
|
+
from the original request are carried through in a standards-compliant way
|
64
|
+
by the cookie jar.
|
46
65
|
- Don't set basic auth header if explicit `Authorization` header is specified
|
47
66
|
- Add `:proxy` option to requests, which can be used for thread-safe
|
48
67
|
per-request proxy configuration, overriding `RestClient.proxy`
|
@@ -54,16 +73,29 @@ This release is largely API compatible, but makes several breaking changes.
|
|
54
73
|
treat any object that responds to `.read` as a streaming payload and pass it
|
55
74
|
through to `.body_stream=` on the Net:HTTP object. This massively reduces the
|
56
75
|
memory required for large file uploads.
|
57
|
-
-
|
58
|
-
`
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
76
|
+
- Changes to redirection behavior:
|
77
|
+
- Remove `RestClient::MaxRedirectsReached` in favor of the normal
|
78
|
+
`ExceptionWithResponse` subclasses. This makes the response accessible on
|
79
|
+
the exception object as `.response`, making it possible for callers to tell
|
80
|
+
what has actually happened when the redirect limit is reached.
|
81
|
+
- When following HTTP redirection, store a list of each previous response on
|
82
|
+
the response object as `.history`. This makes it possible to access the
|
83
|
+
original response headers and body before the redirection was followed.
|
84
|
+
- Follow redirection consistently, regardless of whether the HTTP method was
|
85
|
+
passed as a symbol or string. Under the hood rest-client now normalizes the
|
86
|
+
HTTP request method to a lowercase string.
|
64
87
|
- Add `:before_execution_proc` option to `RestClient::Request`. This makes it
|
65
88
|
possible to add procs like `RestClient.add_before_execution_proc` to a single
|
66
89
|
request without global state.
|
90
|
+
- Run tests on Travis's beta OS X support.
|
91
|
+
- Make `Request#transmit` a private method, along with a few others.
|
92
|
+
- Refactor URI parsing to happen earlier, in Request initialization.
|
93
|
+
- When adding URL params, handle URLs that already contain params.
|
94
|
+
- Add a few more exception classes for obscure HTTP status codes.
|
95
|
+
- Multipart: use a much more robust multipart boundary with greater entropy.
|
96
|
+
- Make `RestClient::Payload::Base#inspect` stop pretending to be a String.
|
97
|
+
- Add `Request#redacted_uri` and `Request#redacted_url` to display the URI
|
98
|
+
with any password redacted.
|
67
99
|
|
68
100
|
# 2.0.0.rc1
|
69
101
|
|
data/lib/restclient.rb
CHANGED
@@ -13,6 +13,7 @@ require File.dirname(__FILE__) + '/restclient/abstract_response'
|
|
13
13
|
require File.dirname(__FILE__) + '/restclient/response'
|
14
14
|
require File.dirname(__FILE__) + '/restclient/raw_response'
|
15
15
|
require File.dirname(__FILE__) + '/restclient/resource'
|
16
|
+
require File.dirname(__FILE__) + '/restclient/params_array'
|
16
17
|
require File.dirname(__FILE__) + '/restclient/payload'
|
17
18
|
require File.dirname(__FILE__) + '/restclient/windows'
|
18
19
|
|
@@ -93,8 +94,9 @@ module RestClient
|
|
93
94
|
# A global proxy URL to use for all requests. This can be overridden on a
|
94
95
|
# per-request basis by passing `:proxy` to RestClient::Request.
|
95
96
|
def self.proxy
|
96
|
-
@proxy
|
97
|
+
@proxy ||= nil
|
97
98
|
end
|
99
|
+
|
98
100
|
def self.proxy=(value)
|
99
101
|
@proxy = value
|
100
102
|
@proxy_set = true
|
@@ -106,7 +108,7 @@ module RestClient
|
|
106
108
|
# @return [Boolean]
|
107
109
|
#
|
108
110
|
def self.proxy_set?
|
109
|
-
|
111
|
+
@proxy_set ||= false
|
110
112
|
end
|
111
113
|
|
112
114
|
# Setup the log for RestClient calls.
|
@@ -5,7 +5,7 @@ module RestClient
|
|
5
5
|
|
6
6
|
module AbstractResponse
|
7
7
|
|
8
|
-
attr_reader :net_http_res, :
|
8
|
+
attr_reader :net_http_res, :request
|
9
9
|
|
10
10
|
def inspect
|
11
11
|
raise NotImplementedError.new('must override in subclass')
|
@@ -31,20 +31,28 @@ module RestClient
|
|
31
31
|
@raw_headers ||= @net_http_res.to_hash
|
32
32
|
end
|
33
33
|
|
34
|
-
def response_set_vars(net_http_res,
|
34
|
+
def response_set_vars(net_http_res, request)
|
35
35
|
@net_http_res = net_http_res
|
36
|
-
@args = args
|
37
36
|
@request = request
|
38
37
|
|
39
38
|
# prime redirection history
|
40
39
|
history
|
41
40
|
end
|
42
41
|
|
43
|
-
# Hash of cookies extracted from response headers
|
42
|
+
# Hash of cookies extracted from response headers.
|
43
|
+
#
|
44
|
+
# NB: This will return only cookies whose domain matches this request, and
|
45
|
+
# may not even return all of those cookies if there are duplicate names.
|
46
|
+
# Use the full cookie_jar for more nuanced access.
|
47
|
+
#
|
48
|
+
# @see #cookie_jar
|
49
|
+
#
|
50
|
+
# @return [Hash]
|
51
|
+
#
|
44
52
|
def cookies
|
45
53
|
hash = {}
|
46
54
|
|
47
|
-
cookie_jar.cookies.each do |cookie|
|
55
|
+
cookie_jar.cookies(@request.uri).each do |cookie|
|
48
56
|
hash[cookie.name] = cookie.value
|
49
57
|
end
|
50
58
|
|
@@ -56,28 +64,40 @@ module RestClient
|
|
56
64
|
# @return [HTTP::CookieJar]
|
57
65
|
#
|
58
66
|
def cookie_jar
|
59
|
-
return @cookie_jar if @cookie_jar
|
67
|
+
return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
|
60
68
|
|
61
|
-
jar =
|
69
|
+
jar = @request.cookie_jar.dup
|
62
70
|
headers.fetch(:set_cookie, []).each do |cookie|
|
63
|
-
jar.parse(cookie, @request.
|
71
|
+
jar.parse(cookie, @request.uri)
|
64
72
|
end
|
65
73
|
|
66
74
|
@cookie_jar = jar
|
67
75
|
end
|
68
76
|
|
69
77
|
# Return the default behavior corresponding to the response code:
|
70
|
-
#
|
78
|
+
#
|
79
|
+
# For 20x status codes: return the response itself
|
80
|
+
#
|
81
|
+
# For 30x status codes:
|
82
|
+
# 301, 302, 307: redirect GET / HEAD if there is a Location header
|
83
|
+
# 303: redirect, changing method to GET, if there is a Location header
|
84
|
+
#
|
85
|
+
# For all other responses, raise a response exception
|
86
|
+
#
|
71
87
|
def return!(&block)
|
72
|
-
|
88
|
+
case code
|
89
|
+
when 200..207
|
73
90
|
self
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
91
|
+
when 301, 302, 307
|
92
|
+
case request.method
|
93
|
+
when 'get', 'head'
|
94
|
+
check_max_redirects
|
78
95
|
follow_redirection(&block)
|
96
|
+
else
|
97
|
+
raise exception_with_response
|
79
98
|
end
|
80
|
-
|
99
|
+
when 303
|
100
|
+
check_max_redirects
|
81
101
|
follow_get_redirection(&block)
|
82
102
|
else
|
83
103
|
raise exception_with_response
|
@@ -96,13 +116,13 @@ module RestClient
|
|
96
116
|
# Follow a redirection response by making a new HTTP request to the
|
97
117
|
# redirection target.
|
98
118
|
def follow_redirection(&block)
|
99
|
-
_follow_redirection(
|
119
|
+
_follow_redirection(request.args.dup, &block)
|
100
120
|
end
|
101
121
|
|
102
122
|
# Follow a redirection response, but change the HTTP method to GET and drop
|
103
123
|
# the payload from the original request.
|
104
124
|
def follow_get_redirection(&block)
|
105
|
-
new_args =
|
125
|
+
new_args = request.args.dup
|
106
126
|
new_args[:method] = :get
|
107
127
|
new_args.delete(:payload)
|
108
128
|
|
@@ -132,7 +152,7 @@ module RestClient
|
|
132
152
|
#
|
133
153
|
def self.beautify_headers(headers)
|
134
154
|
headers.inject({}) do |out, (key, value)|
|
135
|
-
key_sym = key.
|
155
|
+
key_sym = key.tr('-', '_').downcase.to_sym
|
136
156
|
|
137
157
|
# Handle Set-Cookie specially since it cannot be joined by comma.
|
138
158
|
if key.downcase == 'set-cookie'
|
@@ -158,24 +178,24 @@ module RestClient
|
|
158
178
|
# parse location header and merge into existing URL
|
159
179
|
url = headers[:location]
|
160
180
|
|
181
|
+
# cannot follow redirection if there is no location header
|
182
|
+
unless url
|
183
|
+
raise exception_with_response
|
184
|
+
end
|
185
|
+
|
161
186
|
# handle relative redirects
|
162
187
|
unless url.start_with?('http')
|
163
188
|
url = URI.parse(request.url).merge(url).to_s
|
164
189
|
end
|
165
190
|
new_args[:url] = url
|
166
191
|
|
167
|
-
if request.max_redirects <= 0
|
168
|
-
raise exception_with_response
|
169
|
-
end
|
170
192
|
new_args[:password] = request.password
|
171
193
|
new_args[:user] = request.user
|
172
194
|
new_args[:headers] = request.headers
|
173
195
|
new_args[:max_redirects] = request.max_redirects - 1
|
174
196
|
|
175
|
-
#
|
176
|
-
new_args[:
|
177
|
-
cookie_jar.cookies(new_args.fetch(:url)))
|
178
|
-
|
197
|
+
# pass through our new cookie jar
|
198
|
+
new_args[:cookies] = cookie_jar
|
179
199
|
|
180
200
|
# prepare new request
|
181
201
|
new_req = Request.new(new_args)
|
@@ -187,6 +207,12 @@ module RestClient
|
|
187
207
|
new_req.execute(&block)
|
188
208
|
end
|
189
209
|
|
210
|
+
def check_max_redirects
|
211
|
+
if request.max_redirects <= 0
|
212
|
+
raise exception_with_response
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
190
216
|
def exception_with_response
|
191
217
|
begin
|
192
218
|
klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
|
@@ -1,5 +1,19 @@
|
|
1
1
|
module RestClient
|
2
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
|
+
#
|
3
17
|
STATUSES = {100 => 'Continue',
|
4
18
|
101 => 'Switching Protocols',
|
5
19
|
102 => 'Processing', #WebDAV
|
@@ -12,6 +26,8 @@ module RestClient
|
|
12
26
|
205 => 'Reset Content',
|
13
27
|
206 => 'Partial Content',
|
14
28
|
207 => 'Multi-Status', #WebDAV
|
29
|
+
208 => 'Already Reported', # RFC5842
|
30
|
+
226 => 'IM Used', # RFC3229
|
15
31
|
|
16
32
|
300 => 'Multiple Choices',
|
17
33
|
301 => 'Moved Permanently',
|
@@ -21,6 +37,7 @@ module RestClient
|
|
21
37
|
305 => 'Use Proxy', # http/1.1
|
22
38
|
306 => 'Switch Proxy', # no longer used
|
23
39
|
307 => 'Temporary Redirect', # http/1.1
|
40
|
+
308 => 'Permanent Redirect', # RFC7538
|
24
41
|
|
25
42
|
400 => 'Bad Request',
|
26
43
|
401 => 'Unauthorized',
|
@@ -35,10 +52,10 @@ module RestClient
|
|
35
52
|
410 => 'Gone',
|
36
53
|
411 => 'Length Required',
|
37
54
|
412 => 'Precondition Failed',
|
38
|
-
413 => '
|
39
|
-
414 => '
|
55
|
+
413 => 'Payload Too Large', # RFC7231 (renamed, see below)
|
56
|
+
414 => 'URI Too Long', # RFC7231 (renamed, see below)
|
40
57
|
415 => 'Unsupported Media Type',
|
41
|
-
416 => '
|
58
|
+
416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
|
42
59
|
417 => 'Expectation Failed',
|
43
60
|
418 => 'I\'m A Teapot', #RFC2324
|
44
61
|
421 => 'Too Many Connections From This IP',
|
@@ -61,11 +78,28 @@ module RestClient
|
|
61
78
|
505 => 'HTTP Version Not Supported',
|
62
79
|
506 => 'Variant Also Negotiates',
|
63
80
|
507 => 'Insufficient Storage', #WebDAV
|
81
|
+
508 => 'Loop Detected', # RFC5842
|
64
82
|
509 => 'Bandwidth Limit Exceeded', #Apache
|
65
83
|
510 => 'Not Extended',
|
66
84
|
511 => 'Network Authentication Required', # RFC6585
|
67
85
|
}
|
68
86
|
|
87
|
+
STATUSES_COMPATIBILITY = {
|
88
|
+
# The RFCs all specify "Not Found", but "Resource Not Found" was used in
|
89
|
+
# earlier RestClient 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
|
+
|
69
103
|
# This is the base RestClient exception class. Rescue it if you want to
|
70
104
|
# catch any exception that your request might raise
|
71
105
|
# You can get the status code by e.http_code, or see anything about the
|
@@ -148,8 +182,13 @@ module RestClient
|
|
148
182
|
Exceptions::EXCEPTIONS_MAP[code] = klass_constant
|
149
183
|
end
|
150
184
|
|
151
|
-
#
|
152
|
-
|
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
|
153
192
|
|
154
193
|
module Exceptions
|
155
194
|
# We have to split the Exceptions module like we do here because the
|
@@ -197,7 +236,7 @@ module RestClient
|
|
197
236
|
end
|
198
237
|
|
199
238
|
class SSLCertificateNotVerified < Exception
|
200
|
-
def initialize(message)
|
239
|
+
def initialize(message = 'SSL certificate not verified')
|
201
240
|
super nil, nil
|
202
241
|
self.message = message
|
203
242
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RestClient
|
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 RestClient::Utils.encode_query_string
|
11
|
+
# @see RestClient::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
|