rest-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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