rest-core 0.0.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.
Files changed (65) hide show
  1. data/.gitignore +6 -0
  2. data/.gitmodules +3 -0
  3. data/.travis.yml +9 -0
  4. data/CONTRIBUTORS +11 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +201 -0
  7. data/NOTE.md +48 -0
  8. data/README +83 -0
  9. data/README.md +83 -0
  10. data/Rakefile +26 -0
  11. data/TODO.md +17 -0
  12. data/example/facebook.rb +145 -0
  13. data/example/github.rb +21 -0
  14. data/lib/rest-core.rb +48 -0
  15. data/lib/rest-core/app/ask.rb +11 -0
  16. data/lib/rest-core/app/rest-client.rb +24 -0
  17. data/lib/rest-core/builder.rb +24 -0
  18. data/lib/rest-core/client.rb +278 -0
  19. data/lib/rest-core/client/github.rb +19 -0
  20. data/lib/rest-core/client/linkedin.rb +57 -0
  21. data/lib/rest-core/client/rest-graph.rb +262 -0
  22. data/lib/rest-core/client/twitter.rb +59 -0
  23. data/lib/rest-core/client_oauth1.rb +25 -0
  24. data/lib/rest-core/event.rb +17 -0
  25. data/lib/rest-core/middleware.rb +53 -0
  26. data/lib/rest-core/middleware/cache.rb +80 -0
  27. data/lib/rest-core/middleware/common_logger.rb +27 -0
  28. data/lib/rest-core/middleware/default_headers.rb +11 -0
  29. data/lib/rest-core/middleware/default_query.rb +11 -0
  30. data/lib/rest-core/middleware/default_site.rb +15 -0
  31. data/lib/rest-core/middleware/defaults.rb +44 -0
  32. data/lib/rest-core/middleware/error_detector.rb +16 -0
  33. data/lib/rest-core/middleware/error_detector_http.rb +11 -0
  34. data/lib/rest-core/middleware/error_handler.rb +19 -0
  35. data/lib/rest-core/middleware/json_decode.rb +83 -0
  36. data/lib/rest-core/middleware/oauth1_header.rb +81 -0
  37. data/lib/rest-core/middleware/oauth2_query.rb +19 -0
  38. data/lib/rest-core/middleware/timeout.rb +13 -0
  39. data/lib/rest-core/util/hmac.rb +22 -0
  40. data/lib/rest-core/version.rb +4 -0
  41. data/lib/rest-core/wrapper.rb +55 -0
  42. data/lib/rest-graph/config_util.rb +43 -0
  43. data/rest-core.gemspec +162 -0
  44. data/task/.gitignore +1 -0
  45. data/task/gemgem.rb +184 -0
  46. data/test/common.rb +29 -0
  47. data/test/config/rest-graph.yaml +7 -0
  48. data/test/pending/test_load_config.rb +42 -0
  49. data/test/pending/test_multi.rb +123 -0
  50. data/test/pending/test_test_util.rb +86 -0
  51. data/test/test_api.rb +98 -0
  52. data/test/test_cache.rb +62 -0
  53. data/test/test_default.rb +27 -0
  54. data/test/test_error.rb +66 -0
  55. data/test/test_handler.rb +87 -0
  56. data/test/test_misc.rb +75 -0
  57. data/test/test_oauth.rb +42 -0
  58. data/test/test_oauth1_header.rb +46 -0
  59. data/test/test_old.rb +116 -0
  60. data/test/test_page.rb +110 -0
  61. data/test/test_parse.rb +131 -0
  62. data/test/test_rest-graph.rb +10 -0
  63. data/test/test_serialize.rb +44 -0
  64. data/test/test_timeout.rb +25 -0
  65. metadata +267 -0
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ require "#{dir = File.dirname(__FILE__)}/task/gemgem"
4
+ Gemgem.dir = dir
5
+
6
+ ($LOAD_PATH << File.expand_path("#{Gemgem.dir}/lib" )).uniq!
7
+
8
+ desc 'Generate gemspec'
9
+ task 'gem:spec' do
10
+ Gemgem.spec = Gemgem.create do |s|
11
+ require 'rest-core/version'
12
+ s.name = 'rest-core'
13
+ s.version = RestCore::VERSION
14
+ s.homepage = 'https://github.com/cardinalblue/rest-core'
15
+ # s.executables = [s.name]
16
+
17
+ %w[].each{ |g| s.add_runtime_dependency(g) }
18
+ %w[rest-client rack yajl-ruby json json_pure ruby-hmac
19
+ webmock bacon rr rake].each{ |g| s.add_development_dependency(g) }
20
+
21
+ s.authors = ['Cardinal Blue', 'Lin Jen-Shin (godfat)']
22
+ s.email = ['dev (XD) cardinalblue.com']
23
+ end
24
+
25
+ Gemgem.write
26
+ end
data/TODO.md ADDED
@@ -0,0 +1,17 @@
1
+ # TODO
2
+
3
+ ## high
4
+
5
+ * middleware revisit (how to initialize?)
6
+ * what does false and nil mean in env?
7
+
8
+ ## medium
9
+
10
+ * namespace issue
11
+ * what about async request? yield callback? coroutine?
12
+ * dependency?
13
+
14
+ ## low
15
+
16
+ * config loader
17
+ * test utility
@@ -0,0 +1,145 @@
1
+
2
+ # simple client
3
+
4
+ require 'rest-core'
5
+
6
+ RestCore::Builder.client('Facebook', :data, :app_id, :secret, :old_site) do
7
+ s = self.class # this is only for ruby 1.8!
8
+ use s::Timeout , 10
9
+
10
+ use s::DefaultSite , 'https://graph.facebook.com/'
11
+ use s::DefaultHeaders, {'Accept' => 'application/json',
12
+ 'Accept-Language' => 'en-us'}
13
+ use s::Oauth2Query , 'access_token', nil
14
+
15
+ use s::CommonLogger , method(:puts)
16
+ use s::Cache , {}, nil
17
+ use s::ErrorHandler , lambda{ |env| raise ::Facebook::Error.call(env) }
18
+ use s::ErrorDetector , lambda{ |env| env[s::RESPONSE_BODY]['error'] ||
19
+ env[s::RESPONSE_BODY]['error_code'] }
20
+ use s::JsonDecode , true
21
+
22
+ use s::Defaults , :data => lambda{{}},
23
+ :old_site => 'https://api.facebook.com/'
24
+
25
+ run s::RestClient
26
+ end
27
+
28
+ class Facebook::Error < RuntimeError
29
+ include RestCore
30
+
31
+ attr_reader :error, :url
32
+ def initialize error, url=''
33
+ @error, @url = error, url
34
+ super("#{error.inspect} from #{url}")
35
+ end
36
+
37
+ def self.call env
38
+ error, url = env[RESPONSE_BODY], Middleware.request_uri(env)
39
+ new(error, url)
40
+ end
41
+ end
42
+
43
+ module Facebook::Client
44
+ include RestCore
45
+
46
+ def oauth_token
47
+ data['access_token'] || data['oauth_token'] if data.kind_of?(Hash)
48
+ end
49
+ def oauth_token= token
50
+ data['access_token'] = token if data.kind_of?(Hash)
51
+ end
52
+ alias_method :access_token , :oauth_token
53
+ alias_method :access_token=, :oauth_token=
54
+
55
+ def secret_oauth_token ; "#{app_id}|#{secret}" ; end
56
+ alias_method :secret_access_token, :secret_oauth_token
57
+
58
+ def accept ; headers['Accept'] ; end
59
+ def accept= val; headers['Accept'] = val; end
60
+ def lang ; headers['Accept-Language'] ; end
61
+ def lang= val; headers['Accept-Language'] = val; end
62
+
63
+ def authorized? ; !!oauth_token ; end
64
+
65
+ # cookies, app_id, secrect related below
66
+
67
+ def parse_fbs! fbs
68
+ self.data = check_sig_and_return_data(
69
+ # take out facebook sometimes there but sometimes not quotes in cookies
70
+ Rack::Utils.parse_query(fbs.to_s.sub(/^"/, '').sub(/"$/, '')))
71
+ end
72
+
73
+ def fbs
74
+ "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
75
+ end
76
+
77
+ # facebook's new signed_request...
78
+
79
+ def parse_signed_request! request
80
+ sig_encoded, json_encoded = request.split('.')
81
+ sig, json = [sig_encoded, json_encoded].map{ |str|
82
+ "#{str.tr('-_', '+/')}==".unpack('m').first
83
+ }
84
+ self.data = check_sig_and_return_data(
85
+ JsonDecode.json_decode(json).merge('sig' => sig)){
86
+ Hmac.sha256(secret, json_encoded)
87
+ }
88
+ rescue JsonDecode::ParseError
89
+ self.data = nil
90
+ end
91
+
92
+ # oauth related
93
+
94
+ def authorize_url opts={}
95
+ url('oauth/authorize',
96
+ {:client_id => app_id, :access_token => nil}.merge(opts))
97
+ end
98
+
99
+ def authorize! opts={}
100
+ query = {:client_id => app_id, :client_secret => secret}.merge(opts)
101
+ self.data = Rack::Utils.parse_query(
102
+ request({:auto_decode => false}.merge(opts),
103
+ [:get, url('oauth/access_token', query)]))
104
+ end
105
+
106
+ # old rest facebook api, i will definitely love to remove them someday
107
+
108
+ def old_rest path, query={}, opts={}, &cb
109
+ uri = url("method/#{path}", {:format => 'json'}.merge(query),
110
+ {:site => old_site}.merge(opts))
111
+ request(opts, [:get, uri], &cb)
112
+ end
113
+
114
+ def secret_old_rest path, query={}, opts={}, &cb
115
+ old_rest(path, query, {:secret => true}.merge(opts), &cb)
116
+ end
117
+
118
+ def fql code, query={}, opts={}, &cb
119
+ old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
120
+ end
121
+
122
+ def fql_multi codes, query={}, opts={}, &cb
123
+ old_rest('fql.multiquery',
124
+ {:queries => JsonDecode.json_encode(codes)}.merge(query), opts, &cb)
125
+ end
126
+
127
+ protected
128
+ def check_sig_and_return_data cookies
129
+ cookies if secret && if block_given?
130
+ yield
131
+ else
132
+ calculate_sig(cookies)
133
+ end == cookies['sig']
134
+ end
135
+
136
+ def calculate_sig cookies
137
+ Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
138
+ end
139
+
140
+ def fbs_without_sig cookies
141
+ cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
142
+ end
143
+ end
144
+
145
+ Facebook.send(:include, Facebook::Client)
@@ -0,0 +1,21 @@
1
+
2
+ # simple client
3
+
4
+ require 'rest-core'
5
+
6
+ RestCore::Builder.client('Github') do
7
+ s = self.class # this is only for ruby 1.8!
8
+ use s::Timeout , 10
9
+
10
+ use s::DefaultSite , 'https://api.github.com/'
11
+ use s::Oauth2Query , 'access_token', nil
12
+
13
+ use s::CommonLogger , method(:puts)
14
+ use s::Cache , {}, nil
15
+ use s::ErrorHandler , lambda{|env| raise env[s::RESPONSE_BODY]['message']}
16
+ use s::ErrorDetector, lambda{|env| env[s::RESPONSE_HEADERS]['status'].
17
+ first !~ /^2/}
18
+ use s::JsonDecode , true
19
+
20
+ run s::RestClient
21
+ end
@@ -0,0 +1,48 @@
1
+
2
+ module RestCore
3
+ REQUEST_METHOD = 'REQUEST_METHOD'
4
+ REQUEST_PATH = 'REQUEST_PATH'
5
+ REQUEST_QUERY = 'REQUEST_QUERY'
6
+ REQUEST_PAYLOAD = 'REQUEST_PAYLOAD'
7
+ REQUEST_HEADERS = 'REQUEST_HEADERS'
8
+
9
+ RESPONSE_BODY = 'RESPONSE_BODY'
10
+ RESPONSE_STATUS = 'RESPONSE_STATUS'
11
+ RESPONSE_HEADERS = 'RESPONSE_HEADERS'
12
+
13
+ ASK = 'core.ask'
14
+ FAIL = 'core.fail'
15
+ LOG = 'core.log'
16
+
17
+ # core utilities
18
+ autoload :Builder , 'rest-core/builder'
19
+ autoload :Client , 'rest-core/client'
20
+ autoload :Event , 'rest-core/event'
21
+ autoload :Middleware , 'rest-core/middleware'
22
+ autoload :Wrapper , 'rest-core/wrapper'
23
+
24
+ # oauth1 utilities
25
+ autoload :ClientOauth1 , 'rest-core/client_oauth1'
26
+
27
+ # misc utilities
28
+ autoload :Hmac , 'rest-core/util/hmac'
29
+
30
+ # middlewares
31
+ autoload :Cache , 'rest-core/middleware/cache'
32
+ autoload :CommonLogger , 'rest-core/middleware/common_logger'
33
+ autoload :DefaultHeaders, 'rest-core/middleware/default_headers'
34
+ autoload :DefaultQuery , 'rest-core/middleware/default_query'
35
+ autoload :DefaultSite , 'rest-core/middleware/default_site'
36
+ autoload :Defaults , 'rest-core/middleware/defaults'
37
+ autoload :ErrorDetector , 'rest-core/middleware/error_detector'
38
+ autoload :ErrorDetectorHttp, 'rest-core/middleware/error_detector_http'
39
+ autoload :ErrorHandler , 'rest-core/middleware/error_handler'
40
+ autoload :JsonDecode , 'rest-core/middleware/json_decode'
41
+ autoload :Oauth1Header , 'rest-core/middleware/oauth1_header'
42
+ autoload :Oauth2Query , 'rest-core/middleware/oauth2_query'
43
+ autoload :Timeout , 'rest-core/middleware/timeout'
44
+
45
+ # apps
46
+ autoload :Ask , 'rest-core/app/ask'
47
+ autoload :RestClient , 'rest-core/app/rest-client'
48
+ end
@@ -0,0 +1,11 @@
1
+
2
+ require 'rest-core/middleware'
3
+
4
+ require 'restclient'
5
+
6
+ class RestCore::Ask
7
+ include RestCore::Middleware
8
+ def call env
9
+ env
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+
2
+ require 'rest-core/middleware'
3
+
4
+ require 'restclient'
5
+
6
+ class RestCore::RestClient
7
+ include RestCore::Middleware
8
+ def call env
9
+ respond(env,
10
+ ::RestClient::Request.execute(:method => env[REQUEST_METHOD ],
11
+ :url => request_uri(env) ,
12
+ :payload => env[REQUEST_PAYLOAD],
13
+ :headers => env[REQUEST_HEADERS]))
14
+
15
+ rescue ::RestClient::Exception => e
16
+ respond(env, e.response)
17
+ end
18
+
19
+ def respond env, response
20
+ env.merge(RESPONSE_BODY => response.body,
21
+ RESPONSE_STATUS => response.code,
22
+ RESPONSE_HEADERS => response.raw_headers)
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+
2
+ require 'rest-core/client'
3
+ require 'rest-core/wrapper'
4
+
5
+ class RestCore::Builder
6
+ include RestCore
7
+ include Wrapper
8
+
9
+ def self.client prefix, *attrs, &block
10
+ new(&block).to_client(prefix, *attrs)
11
+ end
12
+
13
+ def to_client prefix, *attrs
14
+ # struct = Struct.new(*members, *attrs) if RUBY_VERSION >= 1.9.2
15
+ struct = Struct.new(*(members + attrs))
16
+ client = Class.new(struct)
17
+ client.send(:include, Client)
18
+ Object.const_set( prefix , client)
19
+ client.const_set('Struct', struct)
20
+ class << client; attr_reader :builder; end
21
+ client.instance_variable_set(:@builder, self)
22
+ client
23
+ end
24
+ end
@@ -0,0 +1,278 @@
1
+
2
+ require 'rest-core'
3
+
4
+ module RestCore::Client
5
+ include RestCore
6
+
7
+ Unserializable = [Proc, Method, IO]
8
+
9
+ def self.included mod
10
+ # honor default attributes
11
+ src = mod.members.map{ |name|
12
+ <<-RUBY
13
+ def #{name}
14
+ if (r = super).nil?
15
+ self.#{name} = default_#{name}
16
+ else
17
+ r
18
+ end
19
+ end
20
+
21
+ def default_#{name} app=app
22
+ if app.respond_to?(:#{name})
23
+ app.#{name}({})
24
+ elsif app.respond_to?(:wrapped)
25
+ default_#{name}(app.wrapped) ||
26
+ default_#{name}(app.app)
27
+ elsif app.respond_to?(:app)
28
+ default_#{name}(app.app)
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ private :default_#{name}
34
+
35
+ self
36
+ RUBY
37
+ }
38
+ # if RUBY_VERSION < '1.9.2'
39
+ src << <<-RUBY if mod.members.first.kind_of?(String)
40
+ def members
41
+ super.map(&:to_sym)
42
+ end
43
+ self
44
+ RUBY
45
+ # end
46
+ accessor = Module.new.module_eval(src.join("\n"), __FILE__, __LINE__)
47
+ mod.const_set('Accessor', accessor)
48
+ mod.send(:include, accessor)
49
+ end
50
+
51
+ attr_reader :app, :ask
52
+ def initialize o={}
53
+ @app ||= self.class.builder.to_app
54
+ @ask ||= self.class.builder.to_app(Ask)
55
+ o.each{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
56
+ end
57
+
58
+ def attributes
59
+ Hash[each_pair.map{ |k, v| [k, send(k)] }]
60
+ end
61
+
62
+ def inspect
63
+ "#<struct #{self.class.name} #{attributes.map{ |k, v|
64
+ "#{k}=#{v.inspect}" }.join(', ')}>"
65
+ end
66
+
67
+ def lighten! o={}
68
+ attributes.each{ |k, v| vv = case v;
69
+ when Hash; lighten_hash(v)
70
+ when Array; lighten_array(v)
71
+ when *Unserializable; false
72
+ else v
73
+ end
74
+ send("#{k}=", vv)}
75
+ initialize(o)
76
+ @app, @ask = lighten_app(app), lighten_app(ask)
77
+ self
78
+ end
79
+
80
+ def lighten o={}
81
+ dup.lighten!(o)
82
+ end
83
+
84
+ def url path, query={}, opts={}
85
+ Middleware.request_uri(
86
+ ask.call(build_env({
87
+ REQUEST_PATH => path,
88
+ REQUEST_QUERY => query,
89
+ ASK => true}.merge(opts))))
90
+ end
91
+
92
+ # extra options:
93
+ # auto_decode: Bool # decode with json or not in this API request
94
+ # # default: auto_decode in rest-graph instance
95
+ # timeout: Int # the timeout for this API request
96
+ # # default: timeout in rest-graph instance
97
+ # secret: Bool # use secret_acccess_token or not
98
+ # # default: false
99
+ # cache: Bool # use cache or not; if it's false, update cache, too
100
+ # # default: true
101
+ # expires_in: Int # control when would the cache be expired
102
+ # # default: nil
103
+ # async: Bool # use eventmachine for http client or not
104
+ # # default: false, but true in aget family
105
+ # headers: Hash # additional hash you want to pass
106
+ # # default: {}
107
+ def get path, query={}, opts={}, &cb
108
+ request(opts, [:get , path, query], &cb)
109
+ end
110
+
111
+ def delete path, query={}, opts={}, &cb
112
+ request(opts, [:delete, path, query], &cb)
113
+ end
114
+
115
+ def post path, payload={}, query={}, opts={}, &cb
116
+ request(opts, [:post , path, query, payload], &cb)
117
+ end
118
+
119
+ def put path, payload={}, query={}, opts={}, &cb
120
+ request(opts, [:put , path, query, payload], &cb)
121
+ end
122
+
123
+ # request by eventmachine (em-http)
124
+
125
+ def aget path, query={}, opts={}, &cb
126
+ get(path, query, {:async => true}.merge(opts), &cb)
127
+ end
128
+
129
+ def adelete path, query={}, opts={}, &cb
130
+ delete(path, query, {:async => true}.merge(opts), &cb)
131
+ end
132
+
133
+ def apost path, payload={}, query={}, opts={}, &cb
134
+ post(path, payload, query, {:async => true}.merge(opts), &cb)
135
+ end
136
+
137
+ def aput path, payload={}, query={}, opts={}, &cb
138
+ put(path, payload, query, {:async => true}.merge(opts), &cb)
139
+ end
140
+
141
+ def multi reqs, opts={}, &cb
142
+ request({:async => true}.merge(opts), *reqs, &cb)
143
+ end
144
+
145
+ def request opts, *reqs
146
+ req = reqs.first
147
+ response = app.call(build_env({
148
+ REQUEST_METHOD => req[0] ,
149
+ REQUEST_PATH => req[1] ,
150
+ REQUEST_QUERY => req[2] ,
151
+ REQUEST_PAYLOAD => req[3] ,
152
+ REQUEST_HEADERS => opts['headers'],
153
+ FAIL => [] ,
154
+ LOG => []}.merge(opts)))[RESPONSE_BODY]
155
+
156
+ if block_given?
157
+ yield(response)
158
+ else
159
+ response
160
+ end
161
+ end
162
+ # ------------------------ instance ---------------------
163
+
164
+
165
+
166
+ protected
167
+ def build_env env={}
168
+ string_keys(attributes).merge(string_keys(env))
169
+ end
170
+
171
+ def string_keys hash
172
+ hash.inject({}){ |r, (k, v)|
173
+ if v.kind_of?(Hash)
174
+ r[k.to_s] = case k.to_s
175
+ when REQUEST_QUERY, REQUEST_PAYLOAD, REQUEST_HEADERS
176
+ string_keys(v)
177
+ else; v
178
+ end
179
+ else
180
+ r[k.to_s] = v
181
+ end
182
+ r
183
+ }
184
+ end
185
+
186
+ def lighten_hash hash
187
+ Hash[hash.map{ |(key, value)|
188
+ case value
189
+ when Hash; lighten_hash(value)
190
+ when Array; lighten_array(value)
191
+ when *Unserializable; [key, nil]
192
+ else [key, value]
193
+ end
194
+ }]
195
+ end
196
+
197
+ def lighten_array array
198
+ array.map{ |value|
199
+ case value
200
+ when Hash; lighten_hash(value)
201
+ when Array; lighten_array(value)
202
+ when *Unserializable; nil
203
+ else value
204
+ end
205
+ }.compact
206
+ end
207
+
208
+ def lighten_app app
209
+ members = if app.class.respond_to?(:members)
210
+ app.class.members.map{ |key|
211
+ case value = app.send(key, {})
212
+ when Hash; lighten_hash(value)
213
+ when Array; lighten_array(value)
214
+ when *Unserializable; nil
215
+ else value
216
+ end
217
+ }
218
+ else
219
+ []
220
+ end
221
+
222
+ if app.respond_to?(:app) && app.app
223
+ wrapped = if app.respond_to?(:wrapped) && app.wrapped
224
+ lighten_app(app.wrapped)
225
+ else
226
+ nil
227
+ end
228
+ app.class.new(lighten_app(app.app), *members){
229
+ @wrapped = wrapped if wrapped
230
+ }
231
+ else
232
+ app.class.new(*members)
233
+ end
234
+ end
235
+
236
+ private
237
+ def request_em opts, reqs
238
+ start_time = Time.now
239
+ rs = reqs.map{ |(meth, path, query, payload)|
240
+ r = EM::HttpRequest.new(path).send(meth, :body => payload,
241
+ :head => build_headers(opts),
242
+ :query => query)
243
+ if cached = cache_get(opts, path)
244
+ # TODO: this is hack!!
245
+ r.instance_variable_set('@response', cached)
246
+ r.instance_variable_set('@state' , :finish)
247
+ r.on_request_complete
248
+ r.succeed(r)
249
+ else
250
+ r.callback{
251
+ cache_for(opts, path, meth, r.response)
252
+ log(env.merge('event' =>
253
+ Event::Requested.new(Time.now - start_time, path)))
254
+ }
255
+ r.error{
256
+ log(env.merge('event' =>
257
+ Event::Failed.new(Time.now - start_time, path)))
258
+ }
259
+ end
260
+ r
261
+ }
262
+ EM::MultiRequest.new(rs){ |m|
263
+ # TODO: how to deal with the failed?
264
+ clients = m.responses[:succeeded]
265
+ results = clients.map{ |client|
266
+ post_request(opts, client.uri, client.response)
267
+ }
268
+
269
+ if reqs.size == 1
270
+ yield(results.first)
271
+ else
272
+ log(env.merge('event' => Event::MultiDone.new(Time.now - start_time,
273
+ clients.map(&:uri).join(', '))))
274
+ yield(results)
275
+ end
276
+ }
277
+ end
278
+ end