igrigorik-em-http-request 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ module EventMachine
2
+
3
+ # EventMachine based Multi request client, based on a streaming HTTPRequest class,
4
+ # which allows you to open multiple parallel connections and return only when all
5
+ # of them finish. (i.e. ideal for parallelizing workloads)
6
+ #
7
+ # == Example
8
+ #
9
+ # EventMachine.run {
10
+ #
11
+ # multi = EventMachine::MultiRequest.new
12
+ #
13
+ # # add multiple requests to the multi-handler
14
+ # multi.add(EventMachine::HttpRequest.new('http://www.google.com/').get)
15
+ # multi.add(EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
16
+ #
17
+ # multi.callback {
18
+ # p multi.responses[:succeeded]
19
+ # p multi.responses[:failed]
20
+ #
21
+ # EventMachine.stop
22
+ # }
23
+ # }
24
+ #
25
+
26
+ class MultiRequest
27
+ include EventMachine::Deferrable
28
+
29
+ attr_reader :requests, :responses
30
+
31
+ def initialize
32
+ @requests = []
33
+ @responses = {:succeeded => [], :failed => []}
34
+ end
35
+
36
+ def add(conn)
37
+ conn.callback { @responses[:succeeded].push(conn); check_progress }
38
+ conn.errback { @responses[:failed].push(conn); check_progress }
39
+
40
+ @requests.push(conn)
41
+ end
42
+
43
+ protected
44
+
45
+ # invoke callback if all requests have completed
46
+ def check_progress
47
+ succeed if (@responses[:succeeded].size + @responses[:failed].size) == @requests.size
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require 'uri'
2
+ require 'base64'
3
+
4
+ module EventMachine
5
+
6
+ # EventMachine based HTTP request class with support for streaming consumption
7
+ # of the response. Response is parsed with a Ragel-generated whitelist parser
8
+ # which supports chunked HTTP encoding.
9
+ #
10
+ # == Example
11
+ #
12
+ #
13
+ # EventMachine.run {
14
+ # http = EventMachine::HttpRequest.new('http://127.0.0.1/').get :query => {'keyname' => 'value'}
15
+ #
16
+ # http.callback {
17
+ # p http.response_header.status
18
+ # p http.response_header
19
+ # p http.response
20
+ #
21
+ # EventMachine.stop
22
+ # }
23
+ # }
24
+ #
25
+
26
+ class HttpRequest
27
+ attr_reader :response, :headers
28
+
29
+ def initialize(host, headers = {})
30
+ @headers = headers
31
+ @uri = URI::parse(host)
32
+ end
33
+
34
+ # Send an HTTP request and consume the response. Supported options:
35
+ #
36
+ # head: {Key: Value}
37
+ # Specify an HTTP header, e.g. {'Connection': 'close'}
38
+ #
39
+ # query: {Key: Value}
40
+ # Specify query string parameters (auto-escaped)
41
+ #
42
+ # body: String
43
+ # Specify the request body (you must encode it for now)
44
+ #
45
+
46
+ def get options = {}; send_request(:get, options); end
47
+ def post options = {}; send_request(:post, options); end
48
+
49
+ protected
50
+
51
+ def send_request(method, options)
52
+ raise ArgumentError, "invalid request path" unless /^\// === @uri.path
53
+
54
+ method = method.to_s.upcase
55
+
56
+ EventMachine.connect(@uri.host, @uri.port, EventMachine::HttpClient) { |c|
57
+ c.uri = @uri
58
+ c.method = method
59
+ c.options = options
60
+ }
61
+ end
62
+ end
63
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'pp'
4
+
5
+ require 'lib/em-http'
data/test/stallion.rb ADDED
@@ -0,0 +1,123 @@
1
+ # #--
2
+ # Includes portion originally Copyright (C)2008 Michael Fellinger
3
+ # license See file LICENSE for details
4
+ # #--
5
+
6
+ require 'rack'
7
+
8
+ module Stallion
9
+ class Mount
10
+ def initialize(name, *methods, &block)
11
+ @name, @methods, @block = name, methods, block
12
+ end
13
+
14
+ def ride
15
+ @block.call
16
+ end
17
+
18
+ def match?(request)
19
+ method = request['REQUEST_METHOD']
20
+ right_method = @methods.empty? or @methods.include?(method)
21
+ end
22
+ end
23
+
24
+ class Stable
25
+ attr_reader :request, :response
26
+
27
+ def initialize
28
+ @boxes = {}
29
+ end
30
+
31
+ def in(path, *methods, &block)
32
+ mount = Mount.new(path, *methods, &block)
33
+ @boxes[[path, methods]] = mount
34
+ mount
35
+ end
36
+
37
+ def call(request, response)
38
+ @request, @response = request, response
39
+ @boxes.each do |(path, methods), mount|
40
+ if mount.match?(request)
41
+ mount.ride
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ STABLES = {}
48
+
49
+ def self.saddle(name = nil)
50
+ STABLES[name] = stable = Stable.new
51
+ yield stable
52
+ end
53
+
54
+ def self.run(options = {})
55
+ options = {:Host => "127.0.0.1", :Port => 8080}.merge(options)
56
+ Rack::Handler::Mongrel.run(Rack::Lint.new(self), options)
57
+ end
58
+
59
+ def self.call(env)
60
+ request = Rack::Request.new(env)
61
+ response = Rack::Response.new
62
+
63
+ STABLES.each do |name, stable|
64
+ stable.call(request, response)
65
+ end
66
+
67
+ response.finish
68
+ end
69
+ end
70
+
71
+ Stallion.saddle :spec do |stable|
72
+ stable.in '/' do
73
+
74
+ if stable.request.path_info == '/fail'
75
+ stable.response.status = 404
76
+
77
+ elsif stable.request.query_string == 'q=test'
78
+ stable.response.write 'test'
79
+
80
+ elsif stable.request.path_info == '/echo_query'
81
+ stable.response.write stable.request.query_string
82
+
83
+ elsif stable.request.post?
84
+ stable.response.write 'test'
85
+
86
+ elsif stable.request.path_info == '/gzip'
87
+ io = StringIO.new
88
+ gzip = Zlib::GzipWriter.new(io)
89
+ gzip << "compressed"
90
+ gzip.close
91
+
92
+ stable.response.write io.string
93
+ stable.response["Content-Encoding"] = "gzip"
94
+
95
+ elsif stable.request.path_info == '/deflate'
96
+ stable.response.write Zlib::Deflate.deflate("compressed")
97
+ stable.response["Content-Encoding"] = "deflate"
98
+
99
+ elsif stable.request.env["HTTP_IF_NONE_MATCH"]
100
+ stable.response.status = 304
101
+
102
+ elsif stable.request.env["HTTP_AUTHORIZATION"]
103
+ auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
104
+
105
+ if auth == stable.request.env["HTTP_AUTHORIZATION"]
106
+ stable.response.status = 200
107
+ stable.response.write 'success'
108
+ else
109
+ stable.response.status = 401
110
+ end
111
+
112
+ elsif
113
+ stable.response.write 'Hello, World!'
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ Thread.new do
120
+ Stallion.run :Host => '127.0.0.1', :Port => 8080
121
+ end
122
+
123
+ sleep(2)
@@ -0,0 +1,34 @@
1
+ require 'test/helper'
2
+ require 'test/stallion'
3
+
4
+ describe EventMachine::MultiRequest do
5
+
6
+ def failed
7
+ EventMachine.stop
8
+ fail
9
+ end
10
+
11
+ it "should submit multiple requests in parallel and return once all of them are complete" do
12
+ EventMachine.run {
13
+
14
+ # create an instance of multi-request handler, and the requests themselves
15
+ multi = EventMachine::MultiRequest.new
16
+
17
+ # add multiple requests to the multi-handler
18
+ multi.add(EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get(:query => {:q => 'test'}))
19
+ multi.add(EventMachine::HttpRequest.new('http://169.169.169.169/').get)
20
+
21
+ multi.callback {
22
+ # verify successfull request
23
+ multi.responses[:succeeded].size.should == 1
24
+ multi.responses[:succeeded].first.response.should match(/test/)
25
+
26
+ # verify invalid requests
27
+ multi.responses[:failed].size.should == 1
28
+ multi.responses[:failed].first.response_header.status.should == 0
29
+
30
+ EventMachine.stop
31
+ }
32
+ }
33
+ end
34
+ end
@@ -0,0 +1,192 @@
1
+ require 'test/helper'
2
+ require 'test/stallion'
3
+
4
+ describe EventMachine::HttpRequest do
5
+
6
+ def failed
7
+ EventMachine.stop
8
+ fail
9
+ end
10
+
11
+ it "should fail GET on invalid host" do
12
+ EventMachine.run {
13
+ http = EventMachine::HttpRequest.new('http://169.169.169.169/').get
14
+ http.callback { failed }
15
+ http.errback {
16
+ http.response_header.status.should == 0
17
+ EventMachine.stop
18
+ }
19
+ }
20
+ end
21
+
22
+ it "should fail GET on missing path" do
23
+ EventMachine.run {
24
+ lambda {
25
+ EventMachine::HttpRequest.new('http://www.google.com').get
26
+ }.should raise_error(ArgumentError)
27
+
28
+ EventMachine.stop
29
+ }
30
+ end
31
+
32
+ it "should perform successfull GET" do
33
+ EventMachine.run {
34
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get
35
+
36
+ http.errback { failed }
37
+ http.callback {
38
+ http.response_header.status.should == 200
39
+ http.response.should match(/Hello/)
40
+ EventMachine.stop
41
+ }
42
+ }
43
+ end
44
+
45
+ it "should return 404 on invalid path" do
46
+ EventMachine.run {
47
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/fail').get
48
+
49
+ http.errback { failed }
50
+ http.callback {
51
+ http.response_header.status.should == 404
52
+ EventMachine.stop
53
+ }
54
+ }
55
+ end
56
+
57
+ it "should build query parameters from Hash" do
58
+ EventMachine.run {
59
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => {:q => 'test'}
60
+
61
+ http.errback { failed }
62
+ http.callback {
63
+ http.response_header.status.should == 200
64
+ http.response.should match(/test/)
65
+ EventMachine.stop
66
+ }
67
+ }
68
+ end
69
+
70
+ it "should pass query parameters string" do
71
+ EventMachine.run {
72
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => "q=test"
73
+
74
+ http.errback { failed }
75
+ http.callback {
76
+ http.response_header.status.should == 200
77
+ http.response.should match(/test/)
78
+ EventMachine.stop
79
+ }
80
+ }
81
+ end
82
+
83
+ it "should encode an array of query parameters" do
84
+ EventMachine.run {
85
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/echo_query').get :query => {:hash => ['value1', 'value2']}
86
+
87
+ http.errback { failed }
88
+ http.callback {
89
+ http.response_header.status.should == 200
90
+ http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
91
+ EventMachine.stop
92
+ }
93
+ }
94
+ end
95
+
96
+ it "should perform successfull POST" do
97
+ EventMachine.run {
98
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').post :body => "data"
99
+
100
+ http.errback { failed }
101
+ http.callback {
102
+ http.response_header.status.should == 200
103
+ http.response.should match(/test/)
104
+ EventMachine.stop
105
+ }
106
+ }
107
+ end
108
+
109
+ it "should perform successfull GET with custom header" do
110
+ EventMachine.run {
111
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'if-none-match' => 'evar!'}
112
+
113
+ http.errback { failed }
114
+ http.callback {
115
+ http.response_header.status.should == 304
116
+ EventMachine.stop
117
+ }
118
+ }
119
+ end
120
+
121
+ it "should perform a streaming GET" do
122
+ EventMachine.run {
123
+
124
+ # digg.com uses chunked encoding
125
+ http = EventMachine::HttpRequest.new('http://www.digg.com/').get
126
+
127
+ http.errback { failed }
128
+ http.callback {
129
+ http.response_header.status == 200
130
+ EventMachine.stop
131
+ }
132
+ }
133
+ end
134
+
135
+ it "should perform basic auth" do
136
+ EventMachine.run {
137
+
138
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'authorization' => ['user', 'pass']}
139
+
140
+ http.errback { failed }
141
+ http.callback {
142
+ http.response_header.status == 200
143
+ EventMachine.stop
144
+ }
145
+ }
146
+ end
147
+
148
+ it "should work with keep-alive servers" do
149
+ EventMachine.run {
150
+
151
+ http = EventMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get
152
+
153
+ http.errback { failed }
154
+ http.callback {
155
+ http.response_header.status == 200
156
+ EventMachine.stop
157
+ }
158
+ }
159
+ end
160
+
161
+ it "should detect deflate encoding" do
162
+ EventMachine.run {
163
+
164
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate"}
165
+
166
+ http.errback { failed }
167
+ http.callback {
168
+ http.response_header.status == 200
169
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
170
+ http.response.should == "compressed"
171
+
172
+ EventMachine.stop
173
+ }
174
+ }
175
+ end
176
+
177
+ it "should detect gzip encoding" do
178
+ EventMachine.run {
179
+
180
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/gzip').get :head => {"accept-encoding" => "gzip, compressed"}
181
+
182
+ http.errback { failed }
183
+ http.callback {
184
+ http.response_header.status == 200
185
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
186
+ http.response.should == "compressed"
187
+
188
+ EventMachine.stop
189
+ }
190
+ }
191
+ end
192
+ end