igrigorik-em-http-request 0.1.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.
@@ -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