hurley 0.1
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/.gitignore +4 -0
- data/.travis.yml +28 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.md +20 -0
- data/README.md +317 -0
- data/Rakefile +1 -0
- data/contributors.yaml +8 -0
- data/hurley.gemspec +29 -0
- data/lib/hurley.rb +104 -0
- data/lib/hurley/addressable.rb +9 -0
- data/lib/hurley/client.rb +349 -0
- data/lib/hurley/connection.rb +123 -0
- data/lib/hurley/header.rb +144 -0
- data/lib/hurley/multipart.rb +235 -0
- data/lib/hurley/options.rb +142 -0
- data/lib/hurley/query.rb +252 -0
- data/lib/hurley/tasks.rb +111 -0
- data/lib/hurley/test.rb +101 -0
- data/lib/hurley/test/integration.rb +249 -0
- data/lib/hurley/test/server.rb +102 -0
- data/lib/hurley/url.rb +197 -0
- data/script/bootstrap +2 -0
- data/script/package +7 -0
- data/script/test +168 -0
- data/test/client_test.rb +585 -0
- data/test/header_test.rb +108 -0
- data/test/helper.rb +14 -0
- data/test/live/net_http_test.rb +16 -0
- data/test/multipart_test.rb +306 -0
- data/test/query_test.rb +189 -0
- data/test/test_test.rb +38 -0
- data/test/url_test.rb +443 -0
- metadata +181 -0
data/lib/hurley.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
require "thread"
|
|
3
|
+
|
|
4
|
+
module Hurley
|
|
5
|
+
VERSION = "0.1".freeze
|
|
6
|
+
USER_AGENT = "Hurley v#{VERSION}".freeze
|
|
7
|
+
LIB_PATH = __FILE__[0...-3]
|
|
8
|
+
MUTEX = Mutex.new
|
|
9
|
+
|
|
10
|
+
def self.require_lib(*libs)
|
|
11
|
+
libs.each do |lib|
|
|
12
|
+
require File.join(LIB_PATH, lib)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.default_client
|
|
17
|
+
@default_client ||= mutex { Client.new }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
extend Forwardable
|
|
22
|
+
def_delegators(:default_client,
|
|
23
|
+
:head,
|
|
24
|
+
:get,
|
|
25
|
+
:patch,
|
|
26
|
+
:put,
|
|
27
|
+
:post,
|
|
28
|
+
:delete,
|
|
29
|
+
:options,
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.default_connection
|
|
34
|
+
@default_connection ||= mutex do
|
|
35
|
+
Hurley.require_lib "connection"
|
|
36
|
+
Connection.new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.mutex
|
|
41
|
+
MUTEX.synchronize(&Proc.new)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Error < StandardError; end
|
|
45
|
+
|
|
46
|
+
class ClientError < Error
|
|
47
|
+
attr_reader :response
|
|
48
|
+
|
|
49
|
+
def initialize(ex, response = nil)
|
|
50
|
+
@wrapped_exception = nil
|
|
51
|
+
@response = response
|
|
52
|
+
|
|
53
|
+
if ex.respond_to?(:backtrace)
|
|
54
|
+
super(ex.message)
|
|
55
|
+
@wrapped_exception = ex
|
|
56
|
+
elsif ex.respond_to?(:status_code)
|
|
57
|
+
super("the server responded with status #{ex.status_code}")
|
|
58
|
+
@response = ex
|
|
59
|
+
else
|
|
60
|
+
super(ex.to_s)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def backtrace
|
|
65
|
+
if @wrapped_exception
|
|
66
|
+
@wrapped_exception.backtrace
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def inspect
|
|
73
|
+
%(#<#{self.class}: #{@wrapped_exception.class}>)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class ConnectionFailed < ClientError; end
|
|
78
|
+
class ResourceNotFound < ClientError; end
|
|
79
|
+
class ParsingError < ClientError; end
|
|
80
|
+
|
|
81
|
+
class Timeout < ClientError
|
|
82
|
+
def initialize(ex = nil)
|
|
83
|
+
super(ex || "timeout")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class SSLError < ClientError
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
HTTPS = "https".freeze
|
|
91
|
+
|
|
92
|
+
require_lib(
|
|
93
|
+
"multipart",
|
|
94
|
+
"options",
|
|
95
|
+
"header",
|
|
96
|
+
"url",
|
|
97
|
+
"query",
|
|
98
|
+
"client",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if defined?(Addressable::URI)
|
|
102
|
+
require_lib "addressable"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
require "set"
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Hurley
|
|
6
|
+
class Client
|
|
7
|
+
attr_reader :url
|
|
8
|
+
attr_reader :header
|
|
9
|
+
attr_writer :connection
|
|
10
|
+
attr_reader :request_options
|
|
11
|
+
attr_reader :ssl_options
|
|
12
|
+
|
|
13
|
+
def initialize(endpoint = nil)
|
|
14
|
+
@before_callbacks = []
|
|
15
|
+
@after_callbacks = []
|
|
16
|
+
@url = Url.parse(endpoint)
|
|
17
|
+
@header = Header.new :user_agent => Hurley::USER_AGENT
|
|
18
|
+
@connection = nil
|
|
19
|
+
@request_options = RequestOptions.new
|
|
20
|
+
@ssl_options = SslOptions.new
|
|
21
|
+
yield self if block_given?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
extend Forwardable
|
|
25
|
+
def_delegators(:@url,
|
|
26
|
+
:query,
|
|
27
|
+
:scheme, :scheme=,
|
|
28
|
+
:host, :host=,
|
|
29
|
+
:port, :port=,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def connection
|
|
33
|
+
@connection ||= Hurley.default_connection
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def head(path, query = nil)
|
|
37
|
+
req = request(:head, path)
|
|
38
|
+
req.query.update(query) if query
|
|
39
|
+
yield req if block_given?
|
|
40
|
+
call(req)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get(path, query = nil)
|
|
44
|
+
req = request(:get, path)
|
|
45
|
+
req.query.update(query) if query
|
|
46
|
+
yield req if block_given?
|
|
47
|
+
call(req)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def patch(path, body = nil, ctype = nil)
|
|
51
|
+
req = request(:patch, path)
|
|
52
|
+
req.body = body if body
|
|
53
|
+
req.header[:content_type] = ctype if ctype
|
|
54
|
+
yield req if block_given?
|
|
55
|
+
call(req)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def put(path, body = nil, ctype = nil)
|
|
59
|
+
req = request(:put, path)
|
|
60
|
+
req.body = body if body
|
|
61
|
+
req.header[:content_type] = ctype if ctype
|
|
62
|
+
yield req if block_given?
|
|
63
|
+
call(req)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def post(path, body = nil, ctype = nil)
|
|
67
|
+
req = request(:post, path)
|
|
68
|
+
req.body = body if body
|
|
69
|
+
req.header[:content_type] = ctype if ctype
|
|
70
|
+
yield req if block_given?
|
|
71
|
+
call(req)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(path, query = nil)
|
|
75
|
+
req = request(:delete, path)
|
|
76
|
+
req.query.update(query) if query
|
|
77
|
+
yield req if block_given?
|
|
78
|
+
call(req)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def options(path, query = nil)
|
|
82
|
+
req = request(:options, path)
|
|
83
|
+
req.query.update(query) if query
|
|
84
|
+
yield req if block_given?
|
|
85
|
+
call(req)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def call(request)
|
|
89
|
+
call_with_redirects(request, [])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def before_call(name_or_callback = nil)
|
|
93
|
+
@before_callbacks << (block_given? ?
|
|
94
|
+
NamedCallback.for(name_or_callback, Proc.new) :
|
|
95
|
+
NamedCallback.for(nil, name_or_callback))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def after_call(name_or_callback = nil)
|
|
99
|
+
@after_callbacks << (block_given? ?
|
|
100
|
+
NamedCallback.for(name_or_callback , Proc.new) :
|
|
101
|
+
NamedCallback.for(nil, name_or_callback))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def before_callbacks
|
|
105
|
+
@before_callbacks.map(&:name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def after_callbacks
|
|
109
|
+
@after_callbacks.map(&:name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def request(method, path)
|
|
113
|
+
Request.new(method, Url.join(@url, path), @header.dup, nil, @request_options.dup, @ssl_options.dup)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def call_with_redirects(request, via)
|
|
119
|
+
@before_callbacks.each { |cb| cb.call(request) }
|
|
120
|
+
|
|
121
|
+
request.prepare!
|
|
122
|
+
response = connection.call(request)
|
|
123
|
+
|
|
124
|
+
@after_callbacks.each { |cb| cb.call(response) }
|
|
125
|
+
|
|
126
|
+
if response.automatically_redirect?(via)
|
|
127
|
+
return call_with_redirects(response.location, via << request)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
response.via = via
|
|
131
|
+
response
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class Request < Struct.new(:verb, :url, :header, :body, :options, :ssl_options)
|
|
136
|
+
extend Forwardable
|
|
137
|
+
def_delegators(:url,
|
|
138
|
+
:query,
|
|
139
|
+
:scheme, :scheme=,
|
|
140
|
+
:host, :host=,
|
|
141
|
+
:port, :port=,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def options
|
|
145
|
+
self[:options] ||= RequestOptions.new
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ssl_options
|
|
149
|
+
self[:ssl_options] ||= SslOptions.new
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def query_string
|
|
153
|
+
url.query.to_query_string
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def body_io
|
|
157
|
+
return unless body
|
|
158
|
+
|
|
159
|
+
if body.respond_to?(:read)
|
|
160
|
+
body
|
|
161
|
+
elsif body
|
|
162
|
+
StringIO.new(body)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def on_body(*statuses)
|
|
167
|
+
@body_receiver = [statuses.empty? ? nil : statuses, Proc.new]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def inspect
|
|
171
|
+
"#<%s %s %s>" % [
|
|
172
|
+
self.class.name,
|
|
173
|
+
verb.to_s.upcase,
|
|
174
|
+
url.to_s,
|
|
175
|
+
]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def prepare!
|
|
179
|
+
if value = !header[:authorization] && url.basic_auth
|
|
180
|
+
header[:authorization] = value
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if body
|
|
184
|
+
ctype = nil
|
|
185
|
+
case body
|
|
186
|
+
when Query
|
|
187
|
+
ctype, io = body.to_form
|
|
188
|
+
self.body = io
|
|
189
|
+
when Hash
|
|
190
|
+
ctype, io = options.build_form(body)
|
|
191
|
+
self.body = io
|
|
192
|
+
end
|
|
193
|
+
header[:content_type] ||= ctype || DEFAULT_TYPE
|
|
194
|
+
else
|
|
195
|
+
return unless REQUIRED_BODY_VERBS.include?(verb)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if !header.key?(:content_length) && header[:transfer_encoding] != CHUNKED
|
|
199
|
+
if body
|
|
200
|
+
if sizer = SIZE_METHODS.detect { |method| body.respond_to?(method) }
|
|
201
|
+
header[:content_length] = body.send(sizer).to_i
|
|
202
|
+
else
|
|
203
|
+
header[:transfer_encoding] = CHUNKED
|
|
204
|
+
end
|
|
205
|
+
else
|
|
206
|
+
header[:content_length] = 0
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def body_receiver
|
|
214
|
+
@body_receiver ||= [nil, BodyReceiver.new]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
DEFAULT_TYPE = "application/octet-stream".freeze
|
|
218
|
+
CHUNKED = "chunked".freeze
|
|
219
|
+
REQUIRED_BODY_VERBS = Set.new([:put, :post])
|
|
220
|
+
SIZE_METHODS = [:bytesize, :length, :size]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
class Response
|
|
224
|
+
attr_reader :request
|
|
225
|
+
attr_reader :header
|
|
226
|
+
attr_accessor :body
|
|
227
|
+
attr_accessor :status_code
|
|
228
|
+
attr_writer :via
|
|
229
|
+
|
|
230
|
+
def initialize(request, status_code = nil, header = nil)
|
|
231
|
+
@request = request
|
|
232
|
+
@status_code = status_code
|
|
233
|
+
@header = header || Header.new
|
|
234
|
+
@body = nil
|
|
235
|
+
@receiver = nil
|
|
236
|
+
@timing = nil
|
|
237
|
+
@started_at = Time.now.to_f
|
|
238
|
+
yield self if block_given?
|
|
239
|
+
@ended_at = Time.now.to_f
|
|
240
|
+
if @receiver.respond_to?(:join)
|
|
241
|
+
@body = @receiver.join
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def via
|
|
246
|
+
@via ||= []
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def location
|
|
250
|
+
@location ||= begin
|
|
251
|
+
return unless loc = @header[:location]
|
|
252
|
+
verb = STATUS_FORCE_GET.include?(status_code) ? :get : request.verb
|
|
253
|
+
Request.new(verb, request.url.join(Url.parse(loc)), request.header, request.body, request.options, request.ssl_options)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def status_type
|
|
258
|
+
@status_type ||= STATUS_TYPES.detect { |t| send("#{t}?") } || :other
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def redirection?
|
|
262
|
+
STATUS_REDIRECTION.include?(status_code)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def success?
|
|
266
|
+
status_code > 199 && status_code < 300
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def client_error?
|
|
270
|
+
status_code > 399 && status_code < 500
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def server_error?
|
|
274
|
+
status_code > 499 && status_code < 600
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def automatically_redirect?(previous_requests = nil)
|
|
278
|
+
return false unless redirection?
|
|
279
|
+
limit = request.options.redirection_limit.to_i
|
|
280
|
+
limit > 0 && Array(previous_requests).size < limit
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def receive_body(chunk)
|
|
284
|
+
return if chunk.nil?
|
|
285
|
+
|
|
286
|
+
if @receiver.nil?
|
|
287
|
+
statuses, receiver = request.send(:body_receiver)
|
|
288
|
+
@receiver = if statuses && !statuses.include?(@status_code)
|
|
289
|
+
BodyReceiver.new
|
|
290
|
+
else
|
|
291
|
+
receiver
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
@receiver.call(self, chunk)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def ms
|
|
299
|
+
@timing ||= ((@ended_at - @started_at) * 1000).to_i
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def inspect
|
|
303
|
+
"#<%s %s %s == %d%s %dms>" % [
|
|
304
|
+
self.class.name,
|
|
305
|
+
@request.verb.to_s.upcase,
|
|
306
|
+
@request.url.to_s,
|
|
307
|
+
@status_code.to_i,
|
|
308
|
+
@body ? " (#{@body.bytesize} bytes)" : nil,
|
|
309
|
+
ms,
|
|
310
|
+
]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
STATUS_TYPES = [:success, :redirection, :client_error, :server_error]
|
|
314
|
+
STATUS_FORCE_GET = Set.new([301, 302, 303])
|
|
315
|
+
STATUS_REDIRECTION = STATUS_FORCE_GET + [307, 308]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
class BodyReceiver
|
|
319
|
+
def initialize
|
|
320
|
+
@chunks = []
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def call(res, chunk)
|
|
324
|
+
@chunks << chunk
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def join
|
|
328
|
+
@chunks.join
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class NamedCallback < Struct.new(:name, :callback)
|
|
333
|
+
def self.for(name, callback)
|
|
334
|
+
if callback.respond_to?(:name) && !name
|
|
335
|
+
callback
|
|
336
|
+
else
|
|
337
|
+
new(name, callback)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def name
|
|
342
|
+
self[:name] ||= callback.inspect
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def call(arg)
|
|
346
|
+
callback.call(arg)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|