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.
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,9 @@
1
+ # Enables Addressable::URI support in Hurley
2
+
3
+ require "addressable/uri"
4
+
5
+ module Hurley
6
+ class Url
7
+ @@parser = Addressable::URI.method(:parse)
8
+ end
9
+ 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