igrigorik-em-http-request 0.1.3 → 0.1.5
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/.gitignore +4 -0
- data/README.rdoc +17 -0
- data/em-http-request.gemspec +42 -0
- data/lib/em-http.rb +3 -2
- data/lib/em-http/client.rb +44 -14
- data/lib/em-http/core_ext/hash.rb +53 -0
- data/lib/em-http/decoders.rb +119 -0
- data/lib/em-http/request.rb +6 -2
- data/test/hash.rb +17 -0
- data/test/stallion.rb +1 -1
- data/test/test_request.rb +51 -3
- metadata +6 -1
data/.gitignore
ADDED
data/README.rdoc
CHANGED
@@ -50,3 +50,20 @@ EventMachine based HTTP Request interface. Supports streaming response processin
|
|
50
50
|
}
|
51
51
|
}
|
52
52
|
|
53
|
+
== POST example
|
54
|
+
|
55
|
+
EventMachine.run {
|
56
|
+
http1 = EventMachine::HttpRequest.new('http://www.website.com/').post :body => {"key1" => 1, "key2" => [2,3]}
|
57
|
+
http2 = EventMachine::HttpRequest.new('http://www.website.com/').post :body => "some data"
|
58
|
+
|
59
|
+
# ...
|
60
|
+
}
|
61
|
+
|
62
|
+
== Streaming body processing
|
63
|
+
EventMachine.run {
|
64
|
+
body = ''
|
65
|
+
on_body = lambda { |chunk| body += chunk }
|
66
|
+
http = EventMachine::HttpRequest.new('http://www.website.com/').get :on_response => on_body
|
67
|
+
|
68
|
+
# ...
|
69
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = 'em-http-request'
|
3
|
+
s.version = '0.1.5'
|
4
|
+
s.date = '2009-03-20'
|
5
|
+
s.summary = 'EventMachine based HTTP Request interface'
|
6
|
+
s.description = s.summary
|
7
|
+
s.email = 'ilya@igvita.com'
|
8
|
+
s.homepage = "http://github.com/igrigorik/em-http-request"
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["Ilya Grigorik"]
|
11
|
+
s.add_dependency('eventmachine', '>= 0.12.2')
|
12
|
+
s.extensions = ["ext/buffer/extconf.rb" , "ext/http11_client/extconf.rb"]
|
13
|
+
s.rubyforge_project = "em-http-request"
|
14
|
+
|
15
|
+
# ruby -rpp -e' pp `git ls-files`.split("\n") '
|
16
|
+
s.files = [
|
17
|
+
".autotest",
|
18
|
+
".gitignore",
|
19
|
+
"LICENSE",
|
20
|
+
"README.rdoc",
|
21
|
+
"Rakefile",
|
22
|
+
"em-http-request.gemspec",
|
23
|
+
"ext/buffer/em_buffer.c",
|
24
|
+
"ext/buffer/extconf.rb",
|
25
|
+
"ext/http11_client/ext_help.h",
|
26
|
+
"ext/http11_client/extconf.rb",
|
27
|
+
"ext/http11_client/http11_client.c",
|
28
|
+
"ext/http11_client/http11_parser.c",
|
29
|
+
"ext/http11_client/http11_parser.h",
|
30
|
+
"ext/http11_client/http11_parser.rl",
|
31
|
+
"lib/em-http.rb",
|
32
|
+
"lib/em-http/client.rb",
|
33
|
+
"lib/em-http/core_ext/hash.rb",
|
34
|
+
"lib/em-http/decoders.rb",
|
35
|
+
"lib/em-http/multi.rb",
|
36
|
+
"lib/em-http/request.rb",
|
37
|
+
"test/hash.rb",
|
38
|
+
"test/helper.rb",
|
39
|
+
"test/stallion.rb",
|
40
|
+
"test/test_multi.rb",
|
41
|
+
"test/test_request.rb"]
|
42
|
+
end
|
data/lib/em-http.rb
CHANGED
@@ -6,12 +6,13 @@
|
|
6
6
|
|
7
7
|
require 'rubygems'
|
8
8
|
require 'eventmachine'
|
9
|
-
require 'zlib'
|
10
9
|
|
11
10
|
|
12
11
|
require File.dirname(__FILE__) + '/http11_client'
|
13
12
|
require File.dirname(__FILE__) + '/em_buffer'
|
14
13
|
|
14
|
+
require File.dirname(__FILE__) + '/em-http/core_ext/hash'
|
15
15
|
require File.dirname(__FILE__) + '/em-http/client'
|
16
16
|
require File.dirname(__FILE__) + '/em-http/multi'
|
17
|
-
require File.dirname(__FILE__) + '/em-http/request'
|
17
|
+
require File.dirname(__FILE__) + '/em-http/request'
|
18
|
+
require File.dirname(__FILE__) + '/em-http/decoders'
|
data/lib/em-http/client.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- coding: undecided -*-
|
1
2
|
# #--
|
2
3
|
# Copyright (C)2008 Ilya Grigorik
|
3
4
|
#
|
@@ -162,9 +163,10 @@ module EventMachine
|
|
162
163
|
|
163
164
|
@state = :response_header
|
164
165
|
@parser_nbytes = 0
|
165
|
-
@inflate = []
|
166
166
|
@response = ''
|
167
|
+
@inflate = []
|
167
168
|
@errors = ''
|
169
|
+
@content_decoder = nil
|
168
170
|
end
|
169
171
|
|
170
172
|
# start HTTP request once we establish connection to host
|
@@ -175,16 +177,11 @@ module EventMachine
|
|
175
177
|
|
176
178
|
# request is done, invoke the callback
|
177
179
|
def on_request_complete
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
@response = Zlib::Inflate.inflate(@response)
|
183
|
-
when 'gzip', 'compressed' then
|
184
|
-
@response = Zlib::GzipReader.new(StringIO.new(@response)).read
|
185
|
-
end
|
180
|
+
begin
|
181
|
+
@content_decoder.finalize! if @content_decoder
|
182
|
+
rescue HttpDecoders::DecoderError
|
183
|
+
on_error "Content-decoder error"
|
186
184
|
end
|
187
|
-
|
188
185
|
unbind
|
189
186
|
end
|
190
187
|
|
@@ -222,7 +219,13 @@ module EventMachine
|
|
222
219
|
end
|
223
220
|
|
224
221
|
def send_request_body
|
225
|
-
|
222
|
+
return unless @options[:body]
|
223
|
+
if @options[:body].is_a? Hash
|
224
|
+
body = @options[:body].to_params
|
225
|
+
else
|
226
|
+
body = @options[:body]
|
227
|
+
end
|
228
|
+
send_data body
|
226
229
|
end
|
227
230
|
|
228
231
|
def receive_data(data)
|
@@ -232,11 +235,27 @@ module EventMachine
|
|
232
235
|
|
233
236
|
# Called when part of the body has been read
|
234
237
|
def on_body_data(data)
|
235
|
-
@
|
238
|
+
if @content_decoder
|
239
|
+
begin
|
240
|
+
@content_decoder << data
|
241
|
+
rescue HttpDecoders::DecoderError
|
242
|
+
on_error "Content-decoder error"
|
243
|
+
end
|
244
|
+
else
|
245
|
+
on_decoded_body_data(data)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def on_decoded_body_data(data)
|
250
|
+
if (on_response = @options[:on_response])
|
251
|
+
on_response.call(data)
|
252
|
+
else
|
253
|
+
@response << data
|
254
|
+
end
|
236
255
|
end
|
237
256
|
|
238
257
|
def unbind
|
239
|
-
(@state == :finished) ? succeed : fail
|
258
|
+
(@state == :finished) ? succeed(self) : fail
|
240
259
|
close_connection
|
241
260
|
end
|
242
261
|
|
@@ -301,6 +320,15 @@ module EventMachine
|
|
301
320
|
@bytes_remaining = @response_header.content_length
|
302
321
|
end
|
303
322
|
|
323
|
+
if @inflate.include?(response_header[CONTENT_ENCODING]) &&
|
324
|
+
decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
|
325
|
+
begin
|
326
|
+
@content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
|
327
|
+
rescue HttpDecoders::DecoderError
|
328
|
+
on_error "Content-decoder error"
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
304
332
|
true
|
305
333
|
end
|
306
334
|
|
@@ -401,6 +429,8 @@ module EventMachine
|
|
401
429
|
|
402
430
|
false
|
403
431
|
end
|
432
|
+
|
433
|
+
|
404
434
|
end
|
405
435
|
|
406
|
-
end
|
436
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Hash
|
2
|
+
# Stolen partially from Merb : http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html
|
3
|
+
# Convert this hash to a query string:
|
4
|
+
#
|
5
|
+
# { :name => "Bob",
|
6
|
+
# :address => {
|
7
|
+
# :street => '111 Ruby Ave.',
|
8
|
+
# :city => 'Ruby Central',
|
9
|
+
# :phones => ['111-111-1111', '222-222-2222']
|
10
|
+
# }
|
11
|
+
# }.to_params
|
12
|
+
# #=> "name=Bob&address[city]=Ruby Central&address[phones]=111-111-1111222-222-2222&address[street]=111 Ruby Ave."
|
13
|
+
#
|
14
|
+
def to_params
|
15
|
+
params = ''
|
16
|
+
stack = []
|
17
|
+
|
18
|
+
each do |k, v|
|
19
|
+
if v.is_a?(Hash)
|
20
|
+
stack << [k,v]
|
21
|
+
elsif v.is_a?(Array)
|
22
|
+
stack << [k,Hash.from_array(v)]
|
23
|
+
else
|
24
|
+
params << "#{k}=#{v}&"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
stack.each do |parent, hash|
|
29
|
+
hash.each do |k, v|
|
30
|
+
if v.is_a?(Hash)
|
31
|
+
stack << ["#{parent}[#{k}]", v]
|
32
|
+
else
|
33
|
+
params << "#{parent}[#{k}]=#{v}&"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
params.chop! # trailing &
|
39
|
+
params
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Builds a hash from an array with keys as array indices.
|
44
|
+
def self.from_array(array = [])
|
45
|
+
h = Hash.new
|
46
|
+
array.size.times do |t|
|
47
|
+
h[t] = array[t]
|
48
|
+
end
|
49
|
+
h
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Provides a unified callback interface to decompression libraries.
|
5
|
+
module EventMachine::HttpDecoders
|
6
|
+
|
7
|
+
class DecoderError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def accepted_encodings
|
12
|
+
DECODERS.inject([]) { |r,d| r + d.encoding_names }
|
13
|
+
end
|
14
|
+
|
15
|
+
def decoder_for_encoding(encoding)
|
16
|
+
DECODERS.each { |d|
|
17
|
+
return d if d.encoding_names.include? encoding
|
18
|
+
}
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Base
|
24
|
+
def self.encoding_names
|
25
|
+
name = to_s.split('::').last.downcase
|
26
|
+
[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# chunk_callback:: [Block] To handle a decompressed chunk
|
31
|
+
def initialize(&chunk_callback)
|
32
|
+
@chunk_callback = chunk_callback
|
33
|
+
end
|
34
|
+
|
35
|
+
def <<(compressed)
|
36
|
+
return unless compressed && compressed.size > 0
|
37
|
+
|
38
|
+
decompressed = decompress(compressed)
|
39
|
+
receive_decompressed decompressed
|
40
|
+
end
|
41
|
+
|
42
|
+
def finalize!
|
43
|
+
decompressed = finalize
|
44
|
+
receive_decompressed decompressed
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def receive_decompressed(decompressed)
|
50
|
+
if decompressed && decompressed.size > 0
|
51
|
+
@chunk_callback.call(decompressed)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
##
|
58
|
+
# Must return a part of decompressed
|
59
|
+
def decompress(compressed)
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# May return last part
|
65
|
+
def finalize
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Deflate < Base
|
71
|
+
def decompress(compressed)
|
72
|
+
begin
|
73
|
+
@zstream ||= Zlib::Inflate.new(nil)
|
74
|
+
@zstream.inflate(compressed)
|
75
|
+
rescue Zlib::Error
|
76
|
+
raise DecoderError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def finalize
|
81
|
+
return nil unless @zstream
|
82
|
+
|
83
|
+
begin
|
84
|
+
r = @zstream.inflate(nil)
|
85
|
+
@zstream.close
|
86
|
+
r
|
87
|
+
rescue Zlib::Error
|
88
|
+
raise DecoderError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Oneshot decompressor, due to lack of a streaming Gzip reader
|
95
|
+
# implementation. We may steal code from Zliby to improve this.
|
96
|
+
#
|
97
|
+
# For now, do not put `gzip' or `compressed' in your accept-encoding
|
98
|
+
# header if you expect much data through the :on_response interface.
|
99
|
+
class GZip < Base
|
100
|
+
def self.encoding_names
|
101
|
+
%w(gzip compressed)
|
102
|
+
end
|
103
|
+
|
104
|
+
def decompress(compressed)
|
105
|
+
@buf ||= ''
|
106
|
+
@buf += compressed
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def finalize
|
111
|
+
Zlib::GzipReader.new(StringIO.new(@buf)).read
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
DECODERS = [Deflate, GZip]
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
|
data/lib/em-http/request.rb
CHANGED
@@ -27,7 +27,7 @@ module EventMachine
|
|
27
27
|
|
28
28
|
def initialize(host, headers = {})
|
29
29
|
@headers = headers
|
30
|
-
@uri = URI::parse(host)
|
30
|
+
@uri = URI::parse(host) unless host.kind_of? URI
|
31
31
|
end
|
32
32
|
|
33
33
|
# Send an HTTP request and consume the response. Supported options:
|
@@ -41,6 +41,10 @@ module EventMachine
|
|
41
41
|
# body: String
|
42
42
|
# Specify the request body (you must encode it for now)
|
43
43
|
#
|
44
|
+
# on_response: Proc
|
45
|
+
# Called for each response body chunk (you may assume HTTP 200
|
46
|
+
# OK then)
|
47
|
+
#
|
44
48
|
|
45
49
|
def get options = {}; send_request(:get, options); end
|
46
50
|
def post options = {}; send_request(:post, options); end
|
@@ -66,4 +70,4 @@ module EventMachine
|
|
66
70
|
end
|
67
71
|
end
|
68
72
|
end
|
69
|
-
end
|
73
|
+
end
|
data/test/hash.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
describe Hash do
|
4
|
+
describe ".to_params" do
|
5
|
+
it "should transform a basic hash into HTTP POST Params" do
|
6
|
+
{:a => "alpha", :b => "beta"}.to_params.should == "a=alpha&b=beta"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should transform a more complex hash into HTTP POST Params" do
|
10
|
+
{:a => "a", :b => ["c", "d", "e"]}.to_params.should == "a=a&b[0]=c&b[1]=d&b[2]=e"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should transform a very complex hash into HTTP POST Params" do
|
14
|
+
{:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]}.to_params.should == "a=a&b[0][d]=d&b[0][c]=c&b[1][e]=e&b[1][f]=f"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/test/stallion.rb
CHANGED
@@ -81,7 +81,7 @@ Stallion.saddle :spec do |stable|
|
|
81
81
|
stable.response.write stable.request.query_string
|
82
82
|
|
83
83
|
elsif stable.request.post?
|
84
|
-
stable.response.write
|
84
|
+
stable.response.write stable.request.body.read
|
85
85
|
|
86
86
|
elsif stable.request.path_info == '/timeout'
|
87
87
|
sleep(10)
|
data/test/test_request.rb
CHANGED
@@ -19,7 +19,7 @@ describe EventMachine::HttpRequest do
|
|
19
19
|
}
|
20
20
|
end
|
21
21
|
|
22
|
-
|
22
|
+
it "should fail GET on invalid host" do
|
23
23
|
EventMachine.run {
|
24
24
|
http = EventMachine::HttpRequest.new('http://google1.com/').get
|
25
25
|
http.callback { failed }
|
@@ -112,7 +112,21 @@ describe EventMachine::HttpRequest do
|
|
112
112
|
http.errback { failed }
|
113
113
|
http.callback {
|
114
114
|
http.response_header.status.should == 200
|
115
|
-
http.response.should match(/
|
115
|
+
http.response.should match(/data/)
|
116
|
+
EventMachine.stop
|
117
|
+
}
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should perform successfull POST with Ruby Hash/Array as params" do
|
122
|
+
EventMachine.run {
|
123
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').post :body => {"key1" => 1, "key2" => [2,3]}
|
124
|
+
|
125
|
+
http.errback { failed }
|
126
|
+
http.callback {
|
127
|
+
http.response_header.status.should == 200
|
128
|
+
|
129
|
+
http.response.should match(/key1=1&key2\[0\]=2&key2\[1\]=3/)
|
116
130
|
EventMachine.stop
|
117
131
|
}
|
118
132
|
}
|
@@ -214,4 +228,38 @@ describe EventMachine::HttpRequest do
|
|
214
228
|
http.callback { failed }
|
215
229
|
}
|
216
230
|
end
|
217
|
-
|
231
|
+
|
232
|
+
it "should optionally pass the response body progressively" do
|
233
|
+
EventMachine.run {
|
234
|
+
body = ''
|
235
|
+
on_body = lambda { |chunk| body += chunk }
|
236
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :on_response => on_body
|
237
|
+
|
238
|
+
http.errback { failed }
|
239
|
+
http.callback {
|
240
|
+
http.response_header.status.should == 200
|
241
|
+
http.response.should == ''
|
242
|
+
body.should match(/Hello/)
|
243
|
+
EventMachine.stop
|
244
|
+
}
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should optionally pass the deflate-encoded response body progressively" do
|
249
|
+
EventMachine.run {
|
250
|
+
body = ''
|
251
|
+
on_body = lambda { |chunk| body += chunk }
|
252
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate, compressed"},
|
253
|
+
:on_response => on_body
|
254
|
+
|
255
|
+
http.errback { failed }
|
256
|
+
http.callback {
|
257
|
+
http.response_header.status.should == 200
|
258
|
+
http.response_header["CONTENT_ENCODING"].should == "deflate"
|
259
|
+
http.response.should == ''
|
260
|
+
body.should == "compressed"
|
261
|
+
EventMachine.stop
|
262
|
+
}
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: igrigorik-em-http-request
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ilya Grigorik
|
@@ -33,9 +33,11 @@ extra_rdoc_files: []
|
|
33
33
|
|
34
34
|
files:
|
35
35
|
- .autotest
|
36
|
+
- .gitignore
|
36
37
|
- LICENSE
|
37
38
|
- README.rdoc
|
38
39
|
- Rakefile
|
40
|
+
- em-http-request.gemspec
|
39
41
|
- ext/buffer/em_buffer.c
|
40
42
|
- ext/buffer/extconf.rb
|
41
43
|
- ext/http11_client/ext_help.h
|
@@ -46,8 +48,11 @@ files:
|
|
46
48
|
- ext/http11_client/http11_parser.rl
|
47
49
|
- lib/em-http.rb
|
48
50
|
- lib/em-http/client.rb
|
51
|
+
- lib/em-http/core_ext/hash.rb
|
52
|
+
- lib/em-http/decoders.rb
|
49
53
|
- lib/em-http/multi.rb
|
50
54
|
- lib/em-http/request.rb
|
55
|
+
- test/hash.rb
|
51
56
|
- test/helper.rb
|
52
57
|
- test/stallion.rb
|
53
58
|
- test/test_multi.rb
|