igrigorik-em-http-request 0.1.3 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.bundle
2
+ *.o
3
+ Makefile
4
+ mkmf.log
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'
@@ -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
- if @response_header.compressed? and @inflate.include?(response_header[CONTENT_ENCODING])
180
- case response_header[CONTENT_ENCODING]
181
- when 'deflate' then
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
- send_data @options[:body] if @options[:body]
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
- @response << data
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
+
@@ -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 'test'
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
- it "should fail GET on invalid host" do
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(/test/)
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
- end
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.3
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