rest-graph 1.7.0 → 1.8.0

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