rest-graph 1.7.0 → 1.8.0
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.
- data/.gitignore +6 -0
- data/CHANGES +44 -0
- data/CONTRIBUTORS +1 -0
- data/README +221 -191
- data/README.md +367 -0
- data/Rakefile +28 -43
- data/TODO +1 -0
- data/doc/ToC.md +10 -0
- data/doc/dependency.md +78 -0
- data/doc/design.md +206 -0
- data/doc/rails.md +12 -0
- data/doc/test.md +46 -0
- data/doc/tutorial.md +142 -0
- data/example/rails2/Gemfile +13 -0
- data/example/rails2/app/controllers/application_controller.rb +10 -8
- data/example/rails2/app/views/application/helper.html.erb +1 -0
- data/example/rails2/config/boot.rb +16 -0
- data/example/rails2/config/environment.rb +3 -30
- data/example/rails2/config/preinitializer.rb +23 -0
- data/example/rails2/test/functional/application_controller_test.rb +72 -32
- data/example/rails2/test/test_helper.rb +10 -6
- data/example/rails3/Gemfile +13 -0
- data/example/rails3/Rakefile +7 -0
- data/example/rails3/app/controllers/application_controller.rb +118 -0
- data/example/rails3/app/views/application/helper.html.erb +1 -0
- data/example/rails3/config.ru +4 -0
- data/example/rails3/config/application.rb +23 -0
- data/example/rails3/config/environment.rb +5 -0
- data/example/rails3/config/environments/development.rb +26 -0
- data/example/rails3/config/environments/production.rb +49 -0
- data/example/rails3/config/environments/test.rb +30 -0
- data/example/rails3/config/initializers/secret_token.rb +7 -0
- data/example/rails3/config/initializers/session_store.rb +8 -0
- data/example/rails3/config/rest-graph.yaml +11 -0
- data/example/rails3/config/routes.rb +5 -0
- data/example/rails3/test/functional/application_controller_test.rb +183 -0
- data/example/rails3/test/test_helper.rb +18 -0
- data/example/rails3/test/unit/rails_util_test.rb +44 -0
- data/init.rb +1 -1
- data/lib/rest-graph.rb +5 -571
- data/lib/rest-graph/auto_load.rb +3 -3
- data/lib/rest-graph/autoload.rb +3 -3
- data/lib/rest-graph/config_util.rb +43 -0
- data/lib/rest-graph/core.rb +608 -0
- data/lib/rest-graph/facebook_util.rb +74 -0
- data/lib/rest-graph/rails_util.rb +85 -37
- data/lib/rest-graph/test_util.rb +18 -2
- data/lib/rest-graph/version.rb +2 -2
- data/rest-graph.gemspec +42 -47
- data/task/gemgem.rb +155 -0
- data/test/test_api.rb +16 -0
- data/test/test_cache.rb +28 -8
- data/test/test_error.rb +9 -0
- data/test/test_facebook.rb +36 -0
- data/test/test_load_config.rb +16 -14
- data/test/test_misc.rb +4 -4
- data/test/test_parse.rb +10 -4
- metadata +146 -186
- data/Gemfile.lock +0 -45
- data/README.rdoc +0 -337
- data/example/rails2/script/console +0 -3
- data/example/rails2/script/server +0 -3
- data/lib/rest-graph/load_config.rb +0 -41
data/lib/rest-graph/auto_load.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
|
2
|
-
require 'rest-graph
|
3
|
-
|
4
|
-
|
2
|
+
require 'rest-graph'
|
3
|
+
puts "[DEPRECATED] require 'rest-graph/auto_load' is deprecated, " \
|
4
|
+
"now please just use require 'rest-graph'"
|
data/lib/rest-graph/autoload.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
|
2
|
-
require 'rest-graph
|
3
|
-
|
4
|
-
|
2
|
+
require 'rest-graph'
|
3
|
+
puts "[DEPRECATED] require 'rest-graph/autoload' is deprecated, " \
|
4
|
+
"now please just use require 'rest-graph'"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
require 'erb'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'rest-graph/core'
|
6
|
+
|
7
|
+
module RestGraph::ConfigUtil
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def load_config_for_all
|
11
|
+
RestGraph::ConfigUtil.load_config_for_rails if
|
12
|
+
Object.const_defined?(:Rails)
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_config_for_rails app=Rails
|
16
|
+
root = app.root
|
17
|
+
file = ["#{root}/config/rest-graph.yaml", # YAML should use .yaml
|
18
|
+
"#{root}/config/rest-graph.yml"].find{|path| File.exist?(path)}
|
19
|
+
return unless file
|
20
|
+
|
21
|
+
RestGraph::ConfigUtil.load_config(file, Rails.env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_config file, env
|
25
|
+
config = YAML.load(ERB.new(File.read(file)).result(binding))
|
26
|
+
defaults = config[env]
|
27
|
+
return unless defaults
|
28
|
+
|
29
|
+
mod = Module.new
|
30
|
+
mod.module_eval(defaults.inject([]){ |r, (k, v)|
|
31
|
+
# quote strings, leave others free (e.g. false, numbers, etc)
|
32
|
+
r << <<-RUBY
|
33
|
+
def default_#{k}
|
34
|
+
#{v.kind_of?(String) ? "'#{v}'" : v}
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
}.join)
|
38
|
+
|
39
|
+
RestGraph.send(:extend, mod)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
RestGraph.send(:extend, RestGraph::ConfigUtil)
|
@@ -0,0 +1,608 @@
|
|
1
|
+
|
2
|
+
# optional http client
|
3
|
+
begin; require 'restclient' ; rescue LoadError; end
|
4
|
+
begin; require 'em-http-request'; rescue LoadError; end
|
5
|
+
|
6
|
+
# optional gem
|
7
|
+
begin; require 'rack' ; rescue LoadError; end
|
8
|
+
|
9
|
+
# stdlib
|
10
|
+
require 'digest/md5'
|
11
|
+
require 'openssl'
|
12
|
+
|
13
|
+
require 'cgi'
|
14
|
+
require 'timeout'
|
15
|
+
|
16
|
+
# the data structure used in RestGraph
|
17
|
+
RestGraphStruct = Struct.new(:auto_decode, :strict, :timeout,
|
18
|
+
:graph_server, :old_server,
|
19
|
+
:accept, :lang,
|
20
|
+
:app_id, :secret,
|
21
|
+
:data, :cache,
|
22
|
+
:log_method,
|
23
|
+
:log_handler,
|
24
|
+
:error_handler) unless defined?(RestGraphStruct)
|
25
|
+
|
26
|
+
class RestGraph < RestGraphStruct
|
27
|
+
EventStruct = Struct.new(:duration, :url) unless
|
28
|
+
defined?(::RestGraph::EventStruct)
|
29
|
+
|
30
|
+
Attributes = RestGraphStruct.members.map(&:to_sym) unless
|
31
|
+
defined?(::RestGraph::Attributes)
|
32
|
+
|
33
|
+
class Event < EventStruct
|
34
|
+
# self.class.name[/(?<=::)\w+$/] if RUBY_VERSION >= '1.9.2'
|
35
|
+
def name; self.class.name[/::\w+$/].tr(':', ''); end
|
36
|
+
def to_s; "RestGraph: spent #{sprintf('%f', duration)} #{name} #{url}";end
|
37
|
+
end
|
38
|
+
class Event::MultiDone < Event; end
|
39
|
+
class Event::Requested < Event; end
|
40
|
+
class Event::CacheHit < Event; end
|
41
|
+
class Event::Failed < Event; end
|
42
|
+
|
43
|
+
class Error < RuntimeError
|
44
|
+
class AccessToken < Error; end
|
45
|
+
class InvalidAccessToken < AccessToken; end
|
46
|
+
class MissingAccessToken < AccessToken; end
|
47
|
+
|
48
|
+
attr_reader :error, :url
|
49
|
+
def initialize error, url=''
|
50
|
+
@error, @url = error, url
|
51
|
+
super("#{error.inspect} from #{url}")
|
52
|
+
end
|
53
|
+
|
54
|
+
module Util
|
55
|
+
extend self
|
56
|
+
def parse error, url=''
|
57
|
+
return Error.new(error, url) unless error.kind_of?(Hash)
|
58
|
+
if invalid_token?(error)
|
59
|
+
InvalidAccessToken.new(error, url)
|
60
|
+
elsif missing_token?(error)
|
61
|
+
MissingAccessToken.new(error, url)
|
62
|
+
else
|
63
|
+
Error.new(error, url)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def invalid_token? error
|
68
|
+
(%w[OAuthInvalidTokenException
|
69
|
+
OAuthException].include?((error['error'] || {})['type'])) ||
|
70
|
+
(error['error_code'] == 190) # Invalid OAuth 2.0 Access Token
|
71
|
+
end
|
72
|
+
|
73
|
+
def missing_token? error
|
74
|
+
(error['error'] || {})['message'] =~ /^An active access token/ ||
|
75
|
+
(error['error_code'] == 104) # Requires valid signature
|
76
|
+
end
|
77
|
+
end
|
78
|
+
extend Util
|
79
|
+
end
|
80
|
+
|
81
|
+
# honor default attributes
|
82
|
+
Attributes.each{ |name|
|
83
|
+
module_eval <<-RUBY
|
84
|
+
def #{name}
|
85
|
+
(r = super).nil? ? (self.#{name} = self.class.default_#{name}) : r
|
86
|
+
end
|
87
|
+
RUBY
|
88
|
+
}
|
89
|
+
|
90
|
+
# setup defaults
|
91
|
+
module DefaultAttributes
|
92
|
+
extend self
|
93
|
+
def default_auto_decode ; true ; end
|
94
|
+
def default_strict ; false ; end
|
95
|
+
def default_timeout ; 10 ; end
|
96
|
+
def default_graph_server; 'https://graph.facebook.com/'; end
|
97
|
+
def default_old_server ; 'https://api.facebook.com/' ; end
|
98
|
+
def default_accept ; 'text/javascript' ; end
|
99
|
+
def default_lang ; 'en-us' ; end
|
100
|
+
def default_app_id ; nil ; end
|
101
|
+
def default_secret ; nil ; end
|
102
|
+
def default_data ; {} ; end
|
103
|
+
def default_cache ; nil ; end
|
104
|
+
def default_log_method ; nil ; end
|
105
|
+
def default_log_handler ; nil ; end
|
106
|
+
def default_error_handler
|
107
|
+
lambda{ |error, url| raise ::RestGraph::Error.parse(error, url) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
extend DefaultAttributes
|
111
|
+
|
112
|
+
# Fallback to ruby-hmac gem in case system openssl
|
113
|
+
# lib doesn't support SHA256 (OSX 10.5)
|
114
|
+
def self.hmac_sha256 key, data
|
115
|
+
OpenSSL::HMAC.digest('sha256', key, data)
|
116
|
+
rescue RuntimeError
|
117
|
+
require 'hmac-sha2'
|
118
|
+
HMAC::SHA256.digest(key, data)
|
119
|
+
end
|
120
|
+
|
121
|
+
# begin json backend adapter
|
122
|
+
module YajlRuby
|
123
|
+
def self.extended mod
|
124
|
+
mod.const_set(:ParseError, Yajl::ParseError)
|
125
|
+
end
|
126
|
+
def json_encode hash
|
127
|
+
Yajl::Encoder.encode(hash)
|
128
|
+
end
|
129
|
+
def json_decode json
|
130
|
+
Yajl::Parser.parse(json)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
module Json
|
135
|
+
def self.extended mod
|
136
|
+
mod.const_set(:ParseError, JSON::ParserError)
|
137
|
+
end
|
138
|
+
def json_encode hash
|
139
|
+
JSON.dump(hash)
|
140
|
+
end
|
141
|
+
def json_decode json
|
142
|
+
JSON.parse(json)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
module Gsub
|
147
|
+
class ParseError < RuntimeError; end
|
148
|
+
def self.extended mod
|
149
|
+
mod.const_set(:ParseError, Gsub::ParseError)
|
150
|
+
end
|
151
|
+
# only works for flat hash
|
152
|
+
def json_encode hash
|
153
|
+
middle = hash.inject([]){ |r, (k, v)|
|
154
|
+
r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
|
155
|
+
}.join(',')
|
156
|
+
"{#{middle}}"
|
157
|
+
end
|
158
|
+
def json_decode json
|
159
|
+
raise NotImplementedError.new(
|
160
|
+
'You need to install either yajl-ruby, json, or json_pure gem')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.select_json! picked=false
|
165
|
+
if defined?(::Yajl)
|
166
|
+
extend YajlRuby
|
167
|
+
elsif defined?(::JSON)
|
168
|
+
extend Json
|
169
|
+
elsif picked
|
170
|
+
extend Gsub
|
171
|
+
else
|
172
|
+
# pick a json gem if available
|
173
|
+
%w[yajl json].each{ |json|
|
174
|
+
begin
|
175
|
+
require json
|
176
|
+
break
|
177
|
+
rescue LoadError
|
178
|
+
end
|
179
|
+
}
|
180
|
+
select_json!(true)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
select_json! unless respond_to?(:json_decode)
|
184
|
+
# end json backend adapter
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
# common methods
|
191
|
+
|
192
|
+
def initialize o={}
|
193
|
+
(Attributes + [:access_token]).each{ |name|
|
194
|
+
send("#{name}=", o[name]) if o.key?(name)
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
def access_token
|
199
|
+
data['access_token'] || data['oauth_token']
|
200
|
+
end
|
201
|
+
|
202
|
+
def access_token= token
|
203
|
+
data['access_token'] = token
|
204
|
+
end
|
205
|
+
|
206
|
+
def authorized?
|
207
|
+
!!access_token
|
208
|
+
end
|
209
|
+
|
210
|
+
def secret_access_token
|
211
|
+
"#{app_id}|#{secret}"
|
212
|
+
end
|
213
|
+
|
214
|
+
def lighten!
|
215
|
+
[:cache, :log_method, :log_handler, :error_handler].each{ |obj|
|
216
|
+
send("#{obj}=", nil) }
|
217
|
+
self
|
218
|
+
end
|
219
|
+
|
220
|
+
def lighten
|
221
|
+
dup.lighten!
|
222
|
+
end
|
223
|
+
|
224
|
+
def inspect
|
225
|
+
super.gsub(/(\w+)=([^,>]+)/){ |match|
|
226
|
+
value = $2 == 'nil' ? self.class.send("default_#{$1}").inspect : $2
|
227
|
+
"#{$1}=#{value}"
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
# graph api related methods
|
236
|
+
|
237
|
+
def url path, query={}, server=graph_server, opts={}
|
238
|
+
"#{server}#{path}#{build_query_string(query, opts)}"
|
239
|
+
end
|
240
|
+
|
241
|
+
# extra options:
|
242
|
+
# auto_decode: Bool # decode with json or not in this method call
|
243
|
+
# # default: auto_decode in rest-graph instance
|
244
|
+
# secret: Bool # use secret_acccess_token or not
|
245
|
+
# # default: false
|
246
|
+
# cache: Bool # use cache or not; if it's false, update cache, too
|
247
|
+
# # default: true
|
248
|
+
# expires_in: Int # control when would the cache be expired
|
249
|
+
# # default: nothing
|
250
|
+
# async: Bool # use eventmachine for http client or not
|
251
|
+
# # default: false, but true in aget family
|
252
|
+
# headers: Hash # additional hash you want to pass
|
253
|
+
def get path, query={}, opts={}, &cb
|
254
|
+
request(opts, [:get , url(path, query, graph_server, opts)], &cb)
|
255
|
+
end
|
256
|
+
|
257
|
+
def delete path, query={}, opts={}, &cb
|
258
|
+
request(opts, [:delete, url(path, query, graph_server, opts)], &cb)
|
259
|
+
end
|
260
|
+
|
261
|
+
def post path, payload={}, query={}, opts={}, &cb
|
262
|
+
request(opts, [:post , url(path, query, graph_server, opts), payload],
|
263
|
+
&cb)
|
264
|
+
end
|
265
|
+
|
266
|
+
def put path, payload={}, query={}, opts={}, &cb
|
267
|
+
request(opts, [:put , url(path, query, graph_server, opts), payload],
|
268
|
+
&cb)
|
269
|
+
end
|
270
|
+
|
271
|
+
# request by eventmachine (em-http)
|
272
|
+
|
273
|
+
def aget path, query={}, opts={}, &cb
|
274
|
+
get(path, query, {:async => true}.merge(opts), &cb)
|
275
|
+
end
|
276
|
+
|
277
|
+
def adelete path, query={}, opts={}, &cb
|
278
|
+
delete(path, query, {:async => true}.merge(opts), &cb)
|
279
|
+
end
|
280
|
+
|
281
|
+
def apost path, payload={}, query={}, opts={}, &cb
|
282
|
+
post(path, payload, query, {:async => true}.merge(opts), &cb)
|
283
|
+
end
|
284
|
+
|
285
|
+
def aput path, payload={}, query={}, opts={}, &cb
|
286
|
+
put(path, payload, query, {:async => true}.merge(opts), &cb)
|
287
|
+
end
|
288
|
+
|
289
|
+
def multi reqs, opts={}, &cb
|
290
|
+
request({:async => true}.merge(opts),
|
291
|
+
*reqs.map{ |(meth, path, query, payload)|
|
292
|
+
[meth, url(path, query || {}, graph_server, opts), payload]
|
293
|
+
}, &cb)
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
|
298
|
+
|
299
|
+
|
300
|
+
def next_page hash, opts={}, &cb
|
301
|
+
if hash['paging'].kind_of?(Hash) && hash['paging']['next']
|
302
|
+
request(opts, [:get, hash['paging']['next']], &cb)
|
303
|
+
else
|
304
|
+
yield(nil) if block_given?
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def prev_page hash, opts={}, &cb
|
309
|
+
if hash['paging'].kind_of?(Hash) && hash['paging']['previous']
|
310
|
+
request(opts, [:get, hash['paging']['previous']], &cb)
|
311
|
+
else
|
312
|
+
yield(nil) if block_given?
|
313
|
+
end
|
314
|
+
end
|
315
|
+
alias_method :previous_page, :prev_page
|
316
|
+
|
317
|
+
def for_pages hash, pages=1, opts={}, kind=:next_page, &cb
|
318
|
+
if pages > 1
|
319
|
+
merge_data(send(kind, hash, opts){ |result|
|
320
|
+
yield(result.freeze) if block_given?
|
321
|
+
for_pages(result, pages - 1, opts, kind, &cb) if result
|
322
|
+
}, hash)
|
323
|
+
else
|
324
|
+
yield(nil) if block_given?
|
325
|
+
hash
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
|
331
|
+
|
332
|
+
|
333
|
+
# cookies, app_id, secrect related below
|
334
|
+
|
335
|
+
def parse_rack_env! env
|
336
|
+
env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
|
337
|
+
self.data = parse_fbs!($1)
|
338
|
+
end
|
339
|
+
|
340
|
+
def parse_cookies! cookies
|
341
|
+
self.data = parse_fbs!(cookies["fbs_#{app_id}"])
|
342
|
+
end
|
343
|
+
|
344
|
+
def parse_fbs! fbs
|
345
|
+
self.data = check_sig_and_return_data(
|
346
|
+
# take out facebook sometimes there but sometimes not quotes in cookies
|
347
|
+
Rack::Utils.parse_query(fbs.to_s.gsub('"', '')))
|
348
|
+
end
|
349
|
+
|
350
|
+
def parse_json! json
|
351
|
+
self.data = json &&
|
352
|
+
check_sig_and_return_data(self.class.json_decode(json))
|
353
|
+
rescue ParseError
|
354
|
+
self.data = nil
|
355
|
+
end
|
356
|
+
|
357
|
+
def fbs
|
358
|
+
"#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
|
359
|
+
end
|
360
|
+
|
361
|
+
# facebook's new signed_request...
|
362
|
+
|
363
|
+
def parse_signed_request! request
|
364
|
+
sig_encoded, json_encoded = request.split('.')
|
365
|
+
sig, json = [sig_encoded, json_encoded].map{ |str|
|
366
|
+
"#{str.tr('-_', '+/')}==".unpack('m').first
|
367
|
+
}
|
368
|
+
self.data = check_sig_and_return_data(
|
369
|
+
self.class.json_decode(json).merge('sig' => sig)){
|
370
|
+
self.class.hmac_sha256(secret, json_encoded)
|
371
|
+
}
|
372
|
+
rescue ParseError
|
373
|
+
self.data = nil
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
|
378
|
+
|
379
|
+
|
380
|
+
# oauth related
|
381
|
+
|
382
|
+
def authorize_url opts={}
|
383
|
+
query = {:client_id => app_id, :access_token => nil}.merge(opts)
|
384
|
+
"#{graph_server}oauth/authorize#{build_query_string(query)}"
|
385
|
+
end
|
386
|
+
|
387
|
+
def authorize! opts={}
|
388
|
+
query = {:client_id => app_id, :client_secret => secret}.merge(opts)
|
389
|
+
self.data = Rack::Utils.parse_query(
|
390
|
+
request({:auto_decode => false}.merge(opts),
|
391
|
+
[:get, url('oauth/access_token', query)]))
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
|
396
|
+
|
397
|
+
|
398
|
+
# old rest facebook api, i will definitely love to remove them someday
|
399
|
+
|
400
|
+
def old_rest path, query={}, opts={}, &cb
|
401
|
+
request(
|
402
|
+
opts,
|
403
|
+
[:get,
|
404
|
+
url("method/#{path}", {:format => 'json'}.merge(query),
|
405
|
+
old_server, opts)],
|
406
|
+
&cb)
|
407
|
+
end
|
408
|
+
|
409
|
+
def secret_old_rest path, query={}, opts={}, &cb
|
410
|
+
old_rest(path, query, {:secret => true}.merge(opts), &cb)
|
411
|
+
end
|
412
|
+
alias_method :broken_old_rest, :secret_old_rest
|
413
|
+
|
414
|
+
def exchange_sessions query={}, opts={}, &cb
|
415
|
+
q = {:client_id => app_id, :client_secret => secret,
|
416
|
+
:type => 'client_cred'}.merge(query)
|
417
|
+
request(opts, [:post, url('oauth/exchange_sessions', q)], &cb)
|
418
|
+
end
|
419
|
+
|
420
|
+
def fql code, query={}, opts={}, &cb
|
421
|
+
old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
|
422
|
+
end
|
423
|
+
|
424
|
+
def fql_multi codes, query={}, opts={}, &cb
|
425
|
+
old_rest('fql.multiquery',
|
426
|
+
{:queries => self.class.json_encode(codes)}.merge(query), opts, &cb)
|
427
|
+
end
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
|
432
|
+
|
433
|
+
def request opts, *reqs, &cb
|
434
|
+
Timeout.timeout(timeout){
|
435
|
+
reqs.each{ |(meth, uri, payload)|
|
436
|
+
next if meth != :get
|
437
|
+
cache_assign(uri, nil)
|
438
|
+
} if opts[:cache] == false
|
439
|
+
|
440
|
+
if opts[:async]
|
441
|
+
request_em(opts, reqs, &cb)
|
442
|
+
else
|
443
|
+
request_rc(opts, *reqs.first, &cb)
|
444
|
+
end
|
445
|
+
}
|
446
|
+
end
|
447
|
+
|
448
|
+
protected
|
449
|
+
def request_em opts, reqs
|
450
|
+
start_time = Time.now
|
451
|
+
rs = reqs.map{ |(meth, uri, payload)|
|
452
|
+
r = EM::HttpRequest.new(uri).send(meth, :body => payload)
|
453
|
+
if cached = cache_get(uri)
|
454
|
+
# TODO: this is hack!!
|
455
|
+
r.instance_variable_set('@response', cached)
|
456
|
+
r.instance_variable_set('@state' , :finish)
|
457
|
+
r.on_request_complete
|
458
|
+
r.succeed(r)
|
459
|
+
else
|
460
|
+
r.callback{
|
461
|
+
cache_for(uri, r.response, meth, opts)
|
462
|
+
log(Event::Requested.new(Time.now - start_time, uri))
|
463
|
+
}
|
464
|
+
r.error{
|
465
|
+
log(Event::Failed.new(Time.now - start_time, uri))
|
466
|
+
}
|
467
|
+
end
|
468
|
+
r
|
469
|
+
}
|
470
|
+
EM::MultiRequest.new(rs){ |m|
|
471
|
+
# TODO: how to deal with the failed?
|
472
|
+
clients = m.responses[:succeeded]
|
473
|
+
results = clients.map{ |client|
|
474
|
+
post_request(client.response, client.uri, opts)
|
475
|
+
}
|
476
|
+
|
477
|
+
if reqs.size == 1
|
478
|
+
yield(results.first)
|
479
|
+
else
|
480
|
+
log(Event::MultiDone.new(Time.now - start_time,
|
481
|
+
clients.map(&:uri).join(', ')))
|
482
|
+
yield(results)
|
483
|
+
end
|
484
|
+
}
|
485
|
+
end
|
486
|
+
|
487
|
+
def request_rc opts, meth, uri, payload=nil, &cb
|
488
|
+
start_time = Time.now
|
489
|
+
post_request(cache_get(uri) || fetch(meth, uri, payload, opts),
|
490
|
+
uri, opts, &cb)
|
491
|
+
rescue RestClient::Exception => e
|
492
|
+
post_request(e.http_body, uri, opts, &cb)
|
493
|
+
ensure
|
494
|
+
log(Event::Requested.new(Time.now - start_time, uri))
|
495
|
+
end
|
496
|
+
|
497
|
+
def build_query_string query={}, opts={}
|
498
|
+
token = opts[:secret] ? secret_access_token : access_token
|
499
|
+
qq = token ? {:access_token => token}.merge(query) : query
|
500
|
+
q = qq.select{ |k, v| v } # compacting the hash
|
501
|
+
return '' if q.empty?
|
502
|
+
return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
503
|
+
end
|
504
|
+
|
505
|
+
def build_headers opts={}
|
506
|
+
headers = {}
|
507
|
+
headers['Accept'] = accept if accept
|
508
|
+
headers['Accept-Language'] = lang if lang
|
509
|
+
headers.merge(opts[:headers] || {})
|
510
|
+
end
|
511
|
+
|
512
|
+
def post_request result, uri, opts, &cb
|
513
|
+
if decode?(opts)
|
514
|
+
# [this].first is not needed for yajl-ruby
|
515
|
+
decoded = self.class.json_decode("[#{result}]").first
|
516
|
+
check_error(decoded, uri, &cb)
|
517
|
+
else
|
518
|
+
block_given? ? yield(result) : result
|
519
|
+
end
|
520
|
+
rescue ParseError => error
|
521
|
+
error_handler.call(error, uri) if error_handler
|
522
|
+
end
|
523
|
+
|
524
|
+
def decode? opts
|
525
|
+
if opts.has_key?(:auto_decode)
|
526
|
+
opts[:auto_decode]
|
527
|
+
elsif opts.has_key?(:suppress_decode)
|
528
|
+
!opts[:suppress_decode]
|
529
|
+
else
|
530
|
+
auto_decode
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def check_sig_and_return_data cookies
|
535
|
+
cookies if secret && if block_given?
|
536
|
+
yield
|
537
|
+
else
|
538
|
+
calculate_sig(cookies)
|
539
|
+
end == cookies['sig']
|
540
|
+
end
|
541
|
+
|
542
|
+
def check_error hash, uri
|
543
|
+
if error_handler && hash.kind_of?(Hash) &&
|
544
|
+
(hash['error'] || # from graph api
|
545
|
+
hash['error_code']) # from fql
|
546
|
+
cache_assign(uri, nil)
|
547
|
+
error_handler.call(hash, uri)
|
548
|
+
else
|
549
|
+
block_given? ? yield(hash) : hash
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
def calculate_sig cookies
|
554
|
+
Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
|
555
|
+
end
|
556
|
+
|
557
|
+
def fbs_without_sig cookies
|
558
|
+
cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
|
559
|
+
end
|
560
|
+
|
561
|
+
def cache_assign uri, value
|
562
|
+
return unless cache
|
563
|
+
cache[cache_key(uri)] = value
|
564
|
+
end
|
565
|
+
|
566
|
+
def cache_key uri
|
567
|
+
Digest::MD5.hexdigest(uri)
|
568
|
+
end
|
569
|
+
|
570
|
+
def cache_get uri
|
571
|
+
return unless cache
|
572
|
+
start_time = Time.now
|
573
|
+
cache[cache_key(uri)].tap{ |result|
|
574
|
+
log(Event::CacheHit.new(Time.now - start_time, uri)) if result
|
575
|
+
}
|
576
|
+
end
|
577
|
+
|
578
|
+
def cache_for uri, result, meth, opts
|
579
|
+
return if !cache || meth != :get
|
580
|
+
|
581
|
+
if opts[:expires_in].kind_of?(Fixnum) && cache.method(:store).arity == -3
|
582
|
+
cache.store(cache_key(uri), result, :expires_in => opts[:expires_in])
|
583
|
+
else
|
584
|
+
cache_assign(uri, result)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
def fetch meth, uri, payload, opts
|
589
|
+
RestClient::Request.execute(:method => meth, :url => uri,
|
590
|
+
:headers => build_headers(opts),
|
591
|
+
:payload => payload).body.
|
592
|
+
tap{ |result| cache_for(uri, result, meth, opts) }
|
593
|
+
end
|
594
|
+
|
595
|
+
def merge_data lhs, rhs
|
596
|
+
[lhs, rhs].each{ |hash|
|
597
|
+
return rhs.reject{ |k, v| k == 'paging' } if
|
598
|
+
!hash.kind_of?(Hash) || !hash['data'].kind_of?(Array)
|
599
|
+
}
|
600
|
+
lhs['data'].unshift(*rhs['data'])
|
601
|
+
lhs
|
602
|
+
end
|
603
|
+
|
604
|
+
def log event
|
605
|
+
log_handler.call(event) if log_handler
|
606
|
+
log_method .call("DEBUG: #{event}") if log_method
|
607
|
+
end
|
608
|
+
end
|