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 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