julien51-em-http-request 0.1.9

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,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
+
@@ -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,77 @@
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
+
28
+ def initialize(host, headers = {})
29
+ @headers = headers
30
+ @uri = host.kind_of?(URI) ? host : URI::parse(host)
31
+ end
32
+
33
+ # Send an HTTP request and consume the response. Supported options:
34
+ #
35
+ # head: {Key: Value}
36
+ # Specify an HTTP header, e.g. {'Connection': 'close'}
37
+ #
38
+ # query: {Key: Value}
39
+ # Specify query string parameters (auto-escaped)
40
+ #
41
+ # body: String
42
+ # Specify the request body (you must encode it for now)
43
+ #
44
+ # on_response: Proc
45
+ # Called for each response body chunk (you may assume HTTP 200
46
+ # OK then)
47
+ #
48
+ # host: String
49
+ # Manually specify TCP connect host address, independent of
50
+ # Host: header
51
+
52
+ def get options = {}; send_request(:get, options); end
53
+ def post options = {}; send_request(:post, options); end
54
+
55
+ protected
56
+
57
+ def send_request(method, options)
58
+ raise ArgumentError, "invalid request path" unless /^\// === @uri.path
59
+
60
+ method = method.to_s.upcase
61
+ begin
62
+ host = options[:host] || @uri.host
63
+ EventMachine.connect(host, @uri.port, EventMachine::HttpClient) { |c|
64
+ c.uri = @uri
65
+ c.method = method
66
+ c.options = options
67
+ c.comm_inactivity_timeout = options[:timeout] || 5
68
+ }
69
+ rescue RuntimeError => e
70
+ raise e unless e.message == "no connection"
71
+ conn = EventMachine::HttpClient.new("")
72
+ conn.on_error("no connection")
73
+ conn
74
+ end
75
+ end
76
+ end
77
+ 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,137 @@
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.path_info == '/echo_content_length'
84
+ stable.response.write stable.request.content_length
85
+
86
+ elsif stable.request.post?
87
+ stable.response.write stable.request.body.read
88
+
89
+ elsif stable.request.path_info == '/set_cookie'
90
+ stable.response["Set-Cookie"] = "id=1; expires=Tue, 09-Aug-2011 17:53:39 GMT; path=/;"
91
+ stable.response.write "cookie set"
92
+
93
+ elsif stable.request.path_info == '/echo_cookie'
94
+ stable.response.write stable.request.env["HTTP_COOKIE"]
95
+
96
+ elsif stable.request.path_info == '/timeout'
97
+ sleep(10)
98
+ stable.response.write 'timeout'
99
+
100
+ elsif stable.request.path_info == '/gzip'
101
+ io = StringIO.new
102
+ gzip = Zlib::GzipWriter.new(io)
103
+ gzip << "compressed"
104
+ gzip.close
105
+
106
+ stable.response.write io.string
107
+ stable.response["Content-Encoding"] = "gzip"
108
+
109
+ elsif stable.request.path_info == '/deflate'
110
+ stable.response.write Zlib::Deflate.deflate("compressed")
111
+ stable.response["Content-Encoding"] = "deflate"
112
+
113
+ elsif stable.request.env["HTTP_IF_NONE_MATCH"]
114
+ stable.response.status = 304
115
+
116
+ elsif stable.request.env["HTTP_AUTHORIZATION"]
117
+ auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
118
+
119
+ if auth == stable.request.env["HTTP_AUTHORIZATION"]
120
+ stable.response.status = 200
121
+ stable.response.write 'success'
122
+ else
123
+ stable.response.status = 401
124
+ end
125
+
126
+ elsif
127
+ stable.response.write 'Hello, World!'
128
+ end
129
+
130
+ end
131
+ end
132
+
133
+ Thread.new do
134
+ Stallion.run :Host => '127.0.0.1', :Port => 8080
135
+ end
136
+
137
+ sleep(2)