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.
Files changed (63) hide show
  1. data/.gitignore +6 -0
  2. data/CHANGES +44 -0
  3. data/CONTRIBUTORS +1 -0
  4. data/README +221 -191
  5. data/README.md +367 -0
  6. data/Rakefile +28 -43
  7. data/TODO +1 -0
  8. data/doc/ToC.md +10 -0
  9. data/doc/dependency.md +78 -0
  10. data/doc/design.md +206 -0
  11. data/doc/rails.md +12 -0
  12. data/doc/test.md +46 -0
  13. data/doc/tutorial.md +142 -0
  14. data/example/rails2/Gemfile +13 -0
  15. data/example/rails2/app/controllers/application_controller.rb +10 -8
  16. data/example/rails2/app/views/application/helper.html.erb +1 -0
  17. data/example/rails2/config/boot.rb +16 -0
  18. data/example/rails2/config/environment.rb +3 -30
  19. data/example/rails2/config/preinitializer.rb +23 -0
  20. data/example/rails2/test/functional/application_controller_test.rb +72 -32
  21. data/example/rails2/test/test_helper.rb +10 -6
  22. data/example/rails3/Gemfile +13 -0
  23. data/example/rails3/Rakefile +7 -0
  24. data/example/rails3/app/controllers/application_controller.rb +118 -0
  25. data/example/rails3/app/views/application/helper.html.erb +1 -0
  26. data/example/rails3/config.ru +4 -0
  27. data/example/rails3/config/application.rb +23 -0
  28. data/example/rails3/config/environment.rb +5 -0
  29. data/example/rails3/config/environments/development.rb +26 -0
  30. data/example/rails3/config/environments/production.rb +49 -0
  31. data/example/rails3/config/environments/test.rb +30 -0
  32. data/example/rails3/config/initializers/secret_token.rb +7 -0
  33. data/example/rails3/config/initializers/session_store.rb +8 -0
  34. data/example/rails3/config/rest-graph.yaml +11 -0
  35. data/example/rails3/config/routes.rb +5 -0
  36. data/example/rails3/test/functional/application_controller_test.rb +183 -0
  37. data/example/rails3/test/test_helper.rb +18 -0
  38. data/example/rails3/test/unit/rails_util_test.rb +44 -0
  39. data/init.rb +1 -1
  40. data/lib/rest-graph.rb +5 -571
  41. data/lib/rest-graph/auto_load.rb +3 -3
  42. data/lib/rest-graph/autoload.rb +3 -3
  43. data/lib/rest-graph/config_util.rb +43 -0
  44. data/lib/rest-graph/core.rb +608 -0
  45. data/lib/rest-graph/facebook_util.rb +74 -0
  46. data/lib/rest-graph/rails_util.rb +85 -37
  47. data/lib/rest-graph/test_util.rb +18 -2
  48. data/lib/rest-graph/version.rb +2 -2
  49. data/rest-graph.gemspec +42 -47
  50. data/task/gemgem.rb +155 -0
  51. data/test/test_api.rb +16 -0
  52. data/test/test_cache.rb +28 -8
  53. data/test/test_error.rb +9 -0
  54. data/test/test_facebook.rb +36 -0
  55. data/test/test_load_config.rb +16 -14
  56. data/test/test_misc.rb +4 -4
  57. data/test/test_parse.rb +10 -4
  58. metadata +146 -186
  59. data/Gemfile.lock +0 -45
  60. data/README.rdoc +0 -337
  61. data/example/rails2/script/console +0 -3
  62. data/example/rails2/script/server +0 -3
  63. data/lib/rest-graph/load_config.rb +0 -41
@@ -1,4 +1,4 @@
1
1
 
2
- require 'rest-graph/load_config'
3
-
4
- RestGraph::LoadConfig.autoload!
2
+ require 'rest-graph'
3
+ puts "[DEPRECATED] require 'rest-graph/auto_load' is deprecated, " \
4
+ "now please just use require 'rest-graph'"
@@ -1,4 +1,4 @@
1
1
 
2
- require 'rest-graph/load_config'
3
-
4
- RestGraph::LoadConfig.autoload!
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