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
@@ -0,0 +1 @@
1
+ <%= rest_graph.app_id %>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails3::Application
@@ -0,0 +1,23 @@
1
+
2
+ # Set up gems listed in the Gemfile.
3
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4
+
5
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6
+
7
+ require 'action_controller/railtie'
8
+ require 'rails/test_unit/railtie'
9
+
10
+ Bundler.require(:default, Rails.env)
11
+
12
+ module Rails3
13
+ class Application < Rails::Application
14
+ config.encoding = 'utf-8'
15
+
16
+ logger = Logger.new($stdout)
17
+ logger.level = Logger::INFO
18
+ config.logger = logger
19
+
20
+ # Configure sensitive parameters which will be filtered from the log file.
21
+ config.filter_parameters += [:password]
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ Rails3::Application.initialize!
@@ -0,0 +1,26 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # In the development environment your application's code is reloaded on
5
+ # every request. This slows down response time but is perfect for development
6
+ # since you don't have to restart the webserver when you make code changes.
7
+ config.cache_classes = false
8
+
9
+ # Log error messages when you accidentally call methods on nil.
10
+ config.whiny_nils = true
11
+
12
+ # Show full error reports and disable caching
13
+ config.consider_all_requests_local = true
14
+ config.action_view.debug_rjs = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Don't care if the mailer can't send
18
+ config.action_mailer.raise_delivery_errors = false
19
+
20
+ # Print deprecation notices to the Rails logger
21
+ config.active_support.deprecation = :log
22
+
23
+ # Only use best-standards-support built into browsers
24
+ config.action_dispatch.best_standards_support = :builtin
25
+ end
26
+
@@ -0,0 +1,49 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # The production environment is meant for finished, "live" apps.
5
+ # Code is not reloaded between requests
6
+ config.cache_classes = true
7
+
8
+ # Full error reports are disabled and caching is turned on
9
+ config.consider_all_requests_local = false
10
+ config.action_controller.perform_caching = true
11
+
12
+ # Specifies the header that your server uses for sending files
13
+ config.action_dispatch.x_sendfile_header = "X-Sendfile"
14
+
15
+ # For nginx:
16
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17
+
18
+ # If you have no front-end server that supports something like X-Sendfile,
19
+ # just comment this out and Rails will serve the files
20
+
21
+ # See everything in the log (default is :info)
22
+ # config.log_level = :debug
23
+
24
+ # Use a different logger for distributed setups
25
+ # config.logger = SyslogLogger.new
26
+
27
+ # Use a different cache store in production
28
+ # config.cache_store = :mem_cache_store
29
+
30
+ # Disable Rails's static asset server
31
+ # In production, Apache or nginx will already do this
32
+ config.serve_static_assets = false
33
+
34
+ # Enable serving of images, stylesheets, and javascripts from an asset server
35
+ # config.action_controller.asset_host = "http://assets.example.com"
36
+
37
+ # Disable delivery errors, bad email addresses will be ignored
38
+ # config.action_mailer.raise_delivery_errors = false
39
+
40
+ # Enable threaded mode
41
+ # config.threadsafe!
42
+
43
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44
+ # the I18n.default_locale when a translation can not be found)
45
+ config.i18n.fallbacks = true
46
+
47
+ # Send deprecation notices to registered listeners
48
+ config.active_support.deprecation = :notify
49
+ end
@@ -0,0 +1,30 @@
1
+ Rails3::Application.configure do
2
+ # Settings specified here will take precedence over those in config/application.rb
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = true
9
+
10
+ # Log error messages when you accidentally call methods on nil.
11
+ config.whiny_nils = true
12
+
13
+ # Show full error reports and disable caching
14
+ config.consider_all_requests_local = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Raise exceptions instead of rendering exception templates
18
+ config.action_dispatch.show_exceptions = false
19
+
20
+ # Disable request forgery protection in test environment
21
+ config.action_controller.allow_forgery_protection = false
22
+
23
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
24
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
25
+ # like if you have constraints or database-specific column types
26
+ # config.active_record.schema_format = :sql
27
+
28
+ # Print deprecation notices to the stderr
29
+ config.active_support.deprecation = :stderr
30
+ end
@@ -0,0 +1,7 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Your secret key for verifying the integrity of signed cookies.
4
+ # If you change this key, all old signed cookies will become invalid!
5
+ # Make sure the secret is at least 30 characters and all random,
6
+ # no regular words or you'll be exposed to dictionary attacks.
7
+ Rails3::Application.config.secret_token = '74c293b4c5df1981d2f92785aa5538a21b0901293d693e6bb828669cbb1f1d1f5ddd1b3e21325304c90e952a4866a9eec7620379b6f6c27aae0670cdefda97ae'
@@ -0,0 +1,8 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ Rails3::Application.config.session_store :cookie_store, :key => '_rails3_session'
4
+
5
+ # Use the database for sessions instead of the cookie-based default,
6
+ # which shouldn't be used to store highly confidential information
7
+ # (create the session table with "rails generate session_migration")
8
+ # Rails3::Application.config.session_store :active_record_store
@@ -0,0 +1,11 @@
1
+
2
+ development: &default
3
+ app_id: '123'
4
+ secret: '456'
5
+ canvas: 'can'
6
+
7
+ production:
8
+ *default
9
+
10
+ test:
11
+ *default
@@ -0,0 +1,5 @@
1
+
2
+ Rails3::Application.routes.draw do
3
+ root :controller => 'application', :action => 'index'
4
+ match ':action', :controller => 'application'
5
+ end
@@ -0,0 +1,183 @@
1
+
2
+ require 'test_helper'
3
+ require 'webmock'
4
+
5
+ WebMock.disable_net_connect!
6
+
7
+ class ApplicationControllerTest < ActionController::TestCase
8
+ include WebMock::API
9
+
10
+ def setup
11
+ body = rand(2) == 0 ? '{"error":{"type":"OAuthException"}}' :
12
+ '{"error_code":104}'
13
+
14
+ stub_request(:get, 'https://graph.facebook.com/me').
15
+ to_return(:body => body)
16
+ end
17
+
18
+ def teardown
19
+ WebMock.reset!
20
+ end
21
+
22
+ def assert_url expected
23
+ assert_equal(expected, normalize_url(assigns(:rest_graph_authorize_url)))
24
+ if @response.status == 200 # js redirect
25
+ assert_equal(
26
+ expected,
27
+ normalize_url(
28
+ @response.body.match(/window\.top\.location\.href = '(.+?)'/)[1]))
29
+
30
+ assert_equal(
31
+ CGI.escapeHTML(expected),
32
+ normalize_url(
33
+ @response.body.match(/content="0;url=(.+?)"/)[1], '&amp;'))
34
+
35
+ assert_equal(
36
+ CGI.escapeHTML(expected),
37
+ normalize_url(
38
+ @response.body.match(/<a href="(.+?)" target="_top">/)[1], '&amp;'))
39
+ end
40
+ end
41
+
42
+ def test_index
43
+ get(:index)
44
+ assert_response :redirect
45
+
46
+ url = normalize_url(
47
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
48
+ 'scope=&redirect_uri=http%3A%2F%2Ftest.host%2F')
49
+
50
+ assert_url(url)
51
+ end
52
+
53
+ def test_canvas
54
+ get(:canvas)
55
+ assert_response :success
56
+
57
+ url = normalize_url(
58
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
59
+ 'scope=publish_stream&' \
60
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fcan%2Fcanvas')
61
+
62
+ assert_url(url)
63
+ end
64
+
65
+ def test_diff_canvas
66
+ get(:diff_canvas)
67
+ assert_response :success
68
+
69
+ url = normalize_url(
70
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
71
+ 'scope=email&' \
72
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2FToT%2Fdiff_canvas')
73
+
74
+ assert_url(url)
75
+ end
76
+
77
+ def test_iframe_canvas
78
+ get(:iframe_canvas)
79
+ assert_response :success
80
+
81
+ url = normalize_url(
82
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
83
+ 'scope=&' \
84
+ 'redirect_uri=http%3A%2F%2Fapps.facebook.com%2Fzzz%2Fiframe_canvas')
85
+
86
+ assert_url(url)
87
+ end
88
+
89
+ def test_options
90
+ get(:options)
91
+ assert_response :redirect
92
+
93
+ url = normalize_url(
94
+ 'https://graph.facebook.com/oauth/authorize?client_id=123&' \
95
+ 'scope=bogus&' \
96
+ 'redirect_uri=http%3A%2F%2Ftest.host%2Foptions')
97
+
98
+ assert_url(url)
99
+ end
100
+
101
+ def test_protected
102
+ assert_nil @controller.public_methods.find{ |m| m.to_s =~ /^rest_graph/ }
103
+ end
104
+
105
+ def test_no_auto
106
+ get(:no_auto)
107
+ assert_response :success
108
+ assert_equal 'XD', @response.body
109
+ end
110
+
111
+ def test_app_id
112
+ get(:diff_app_id)
113
+ assert_response :success
114
+ assert_equal 'zzz', @response.body
115
+ end
116
+
117
+ def test_cache
118
+ WebMock.reset!
119
+ stub_request(:get, 'https://graph.facebook.com/cache').
120
+ to_return(:body => '{"message":"ok"}')
121
+
122
+ get(:cache)
123
+ assert_response :success
124
+ assert_equal '{"message":"ok"}', @response.body
125
+ end
126
+
127
+ def test_handler
128
+ WebMock.reset!
129
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=aloha').
130
+ to_return(:body => '["snowman"]')
131
+
132
+ Rails.cache[:fbs] = RestGraph.new(:access_token => 'aloha').fbs
133
+ get(:handler_)
134
+ assert_response :success
135
+ assert_equal '["snowman"]', @response.body
136
+ ensure
137
+ Rails.cache.clear
138
+ end
139
+
140
+ def test_session
141
+ WebMock.reset!
142
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=wozilla').
143
+ to_return(:body => '["fireball"]')
144
+
145
+ @request.session[RestGraph::RailsUtil.rest_graph_storage_key] =
146
+ RestGraph.new(:access_token => 'wozilla').fbs
147
+
148
+ get(:session_)
149
+ assert_response :success
150
+ assert_equal '["fireball"]', @response.body
151
+ end
152
+
153
+ def test_cookies
154
+ WebMock.reset!
155
+ stub_request(:get, 'https://graph.facebook.com/me?access_token=blizzard').
156
+ to_return(:body => '["yeti"]')
157
+
158
+ @request.cookies[RestGraph::RailsUtil.rest_graph_storage_key] =
159
+ RestGraph.new(:access_token => 'blizzard').fbs
160
+
161
+ get(:cookies_)
162
+ assert_response :success
163
+ assert_equal '["yeti"]', @response.body
164
+ end
165
+
166
+ def test_error
167
+ get(:error)
168
+ rescue => e
169
+ assert_equal RestGraph::Error, e.class
170
+ end
171
+
172
+ def test_reinitailize
173
+ get(:reinitialize)
174
+ assert_response :success
175
+ assert_equal [nil, {'a' => 'b'}], YAML.load(@response.body)
176
+ end
177
+
178
+ def test_helper
179
+ get(:helper)
180
+ assert_response :success
181
+ assert_equal RestGraph.default_app_id, @response.body.strip
182
+ end
183
+ end
@@ -0,0 +1,18 @@
1
+
2
+ ENV["RAILS_ENV"] = "test"
3
+ require File.expand_path('../../config/environment', __FILE__)
4
+ begin
5
+ require 'rails/test_help'
6
+ rescue LoadError # for rails2
7
+ require 'test_help'
8
+ end
9
+
10
+ class ActiveSupport::TestCase
11
+ def normalize_query query, amp='&'
12
+ '?' + query[1..-1].split(amp).sort.join(amp)
13
+ end
14
+
15
+ def normalize_url url, amp='&'
16
+ url.sub(/\?.+/){ |query| normalize_query(query, amp) }
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'test_helper'
3
+ require 'rr'
4
+
5
+ class RailsUtilTest < ActiveSupport::TestCase
6
+ include RR::Adapters::TestUnit
7
+
8
+ def setup_mock url
9
+ mock(RestGraph::RailsUtil).rest_graph_in_canvas?{ false }
10
+ mock(RestGraph::RailsUtil).request{
11
+ mock(Object.new).url{ url }
12
+ }
13
+ end
14
+
15
+ def test_rest_graph_normalized_request_uri_0
16
+ setup_mock( 'http://test.com/?code=123&lang=en')
17
+ assert_equal('http://test.com/?lang=en',
18
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
19
+ end
20
+
21
+ def test_rest_graph_normalized_request_uri_1
22
+ setup_mock( 'http://test.com/?lang=en&code=123')
23
+ assert_equal('http://test.com/?lang=en',
24
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
25
+ end
26
+
27
+ def test_rest_graph_normalized_request_uri_2
28
+ setup_mock( 'http://test.com/?session=abc&lang=en&code=123')
29
+ assert_equal('http://test.com/?lang=en',
30
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
31
+ end
32
+
33
+ def test_rest_graph_normalized_request_uri_3
34
+ setup_mock( 'http://test.com/?code=123')
35
+ assert_equal('http://test.com/',
36
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
37
+ end
38
+
39
+ def test_rest_graph_normalized_request_uri_4
40
+ setup_mock( 'http://test.com/?signed_request=abc&code=123')
41
+ assert_equal('http://test.com/',
42
+ RestGraph::RailsUtil.rest_graph_normalized_request_uri)
43
+ end
44
+ end
data/init.rb CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- require 'rest-graph/autoload'
2
+ require 'rest-graph'
data/lib/rest-graph.rb CHANGED
@@ -1,573 +1,7 @@
1
1
 
2
- # optional http client
3
- begin; require 'restclient' ; rescue LoadError; end
4
- begin; require 'em-http-request'; rescue LoadError; end
2
+ require 'rest-graph/core'
3
+ require 'rest-graph/config_util'
4
+ require 'rest-graph/facebook_util'
5
+ require 'rest-graph/version'
5
6
 
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
- # for ruby version >= 1.8.7, we can simply pass sha256,
116
- # instead of OpenSSL::Digest::Digest.new('sha256')
117
- # i'll go back to original implementation once all old systems died
118
- OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, data)
119
- rescue RuntimeError
120
- require 'hmac-sha2'
121
- HMAC::SHA256.digest(key, data)
122
- end
123
-
124
- # begin json backend adapter
125
- module YajlRuby
126
- def self.extended mod
127
- mod.const_set(:ParseError, Yajl::ParseError)
128
- end
129
- def json_encode hash
130
- Yajl::Encoder.encode(hash)
131
- end
132
- def json_decode json
133
- Yajl::Parser.parse(json)
134
- end
135
- end
136
-
137
- module Json
138
- def self.extended mod
139
- mod.const_set(:ParseError, JSON::ParserError)
140
- end
141
- def json_encode hash
142
- JSON.dump(hash)
143
- end
144
- def json_decode json
145
- JSON.parse(json)
146
- end
147
- end
148
-
149
- module Gsub
150
- class ParseError < RuntimeError; end
151
- def self.extended mod
152
- mod.const_set(:ParseError, Gsub::ParseError)
153
- end
154
- # only works for flat hash
155
- def json_encode hash
156
- middle = hash.inject([]){ |r, (k, v)|
157
- r << "\"#{k}\":\"#{v.gsub('"','\\"')}\""
158
- }.join(',')
159
- "{#{middle}}"
160
- end
161
- def json_decode json
162
- raise NotImplementedError.new(
163
- 'You need to install either yajl-ruby, json, or json_pure gem')
164
- end
165
- end
166
-
167
- def self.select_json! picked=false
168
- if defined?(::Yajl)
169
- extend YajlRuby
170
- elsif defined?(::JSON)
171
- extend Json
172
- elsif picked
173
- extend Gsub
174
- else
175
- # pick a json gem if available
176
- %w[yajl json].each{ |json|
177
- begin
178
- require json
179
- break
180
- rescue LoadError
181
- end
182
- }
183
- select_json!(true)
184
- end
185
- end
186
- select_json! unless respond_to?(:json_decode)
187
- # end json backend adapter
188
-
189
-
190
-
191
-
192
-
193
- # common methods
194
-
195
- def initialize o={}
196
- (Attributes + [:access_token]).each{ |name|
197
- send("#{name}=", o[name]) if o.key?(name)
198
- }
199
- end
200
-
201
- def access_token
202
- data['access_token'] || data['oauth_token']
203
- end
204
-
205
- def access_token= token
206
- data['access_token'] = token
207
- end
208
-
209
- def authorized?
210
- !!access_token
211
- end
212
-
213
- def secret_access_token
214
- "#{app_id}|#{secret}"
215
- end
216
-
217
- def lighten!
218
- [:cache, :log_method, :log_handler, :error_handler].each{ |obj|
219
- send("#{obj}=", nil) }
220
- self
221
- end
222
-
223
- def lighten
224
- dup.lighten!
225
- end
226
-
227
- def inspect
228
- super.gsub(/(\w+)=([^,>]+)/){ |match|
229
- value = $2 == 'nil' ? self.class.send("default_#{$1}").inspect : $2
230
- "#{$1}=#{value}"
231
- }
232
- end
233
-
234
-
235
-
236
-
237
-
238
- # graph api related methods
239
-
240
- def url path, query={}, server=graph_server, opts={}
241
- "#{server}#{path}#{build_query_string(query, opts)}"
242
- end
243
-
244
- def get path, query={}, opts={}, &cb
245
- request(opts, [:get , url(path, query, graph_server, opts)], &cb)
246
- end
247
-
248
- def delete path, query={}, opts={}, &cb
249
- request(opts, [:delete, url(path, query, graph_server, opts)], &cb)
250
- end
251
-
252
- def post path, payload={}, query={}, opts={}, &cb
253
- request(opts, [:post , url(path, query, graph_server, opts), payload],
254
- &cb)
255
- end
256
-
257
- def put path, payload={}, query={}, opts={}, &cb
258
- request(opts, [:put , url(path, query, graph_server, opts), payload],
259
- &cb)
260
- end
261
-
262
- # request by eventmachine (em-http)
263
-
264
- def aget path, query={}, opts={}, &cb
265
- get(path, query, {:async => true}.merge(opts), &cb)
266
- end
267
-
268
- def adelete path, query={}, opts={}, &cb
269
- delete(path, query, {:async => true}.merge(opts), &cb)
270
- end
271
-
272
- def apost path, payload={}, query={}, opts={}, &cb
273
- post(path, payload, query, {:async => true}.merge(opts), &cb)
274
- end
275
-
276
- def aput path, payload={}, query={}, opts={}, &cb
277
- put(path, payload, query, {:async => true}.merge(opts), &cb)
278
- end
279
-
280
- def multi reqs, opts={}, &cb
281
- request({:async => true}.merge(opts),
282
- *reqs.map{ |(meth, path, query, payload)|
283
- [meth, url(path, query || {}, graph_server, opts), payload]
284
- }, &cb)
285
- end
286
-
287
-
288
-
289
-
290
-
291
- def next_page hash, opts={}, &cb
292
- if hash['paging'].kind_of?(Hash) && hash['paging']['next']
293
- request(opts, [:get, hash['paging']['next']], &cb)
294
- else
295
- yield(nil) if block_given?
296
- end
297
- end
298
-
299
- def prev_page hash, opts={}, &cb
300
- if hash['paging'].kind_of?(Hash) && hash['paging']['previous']
301
- request(opts, [:get, hash['paging']['previous']], &cb)
302
- else
303
- yield(nil) if block_given?
304
- end
305
- end
306
- alias_method :previous_page, :prev_page
307
-
308
- def for_pages hash, pages=1, opts={}, kind=:next_page, &cb
309
- if pages > 1
310
- merge_data(send(kind, hash, opts){ |result|
311
- yield(result.freeze) if block_given?
312
- for_pages(result, pages - 1, opts, kind, &cb) if result
313
- }, hash)
314
- else
315
- yield(nil) if block_given?
316
- hash
317
- end
318
- end
319
-
320
-
321
-
322
-
323
-
324
- # cookies, app_id, secrect related below
325
-
326
- def parse_rack_env! env
327
- env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
328
- self.data = parse_fbs!($1)
329
- end
330
-
331
- def parse_cookies! cookies
332
- self.data = parse_fbs!(cookies["fbs_#{app_id}"])
333
- end
334
-
335
- def parse_fbs! fbs
336
- self.data = check_sig_and_return_data(
337
- # take out facebook sometimes there but sometimes not quotes in cookies
338
- Rack::Utils.parse_query(fbs.to_s.gsub('"', '')))
339
- end
340
-
341
- def parse_json! json
342
- self.data = json &&
343
- check_sig_and_return_data(self.class.json_decode(json))
344
- rescue ParseError
345
- end
346
-
347
- def fbs
348
- "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
349
- end
350
-
351
- # facebook's new signed_request...
352
-
353
- def parse_signed_request! request
354
- sig_encoded, json_encoded = request.split('.')
355
- sig, json = [sig_encoded, json_encoded].map{ |str|
356
- "#{str.tr('-_', '+/')}==".unpack('m').first
357
- }
358
- self.data = self.class.json_decode(json) if
359
- secret && self.class.hmac_sha256(secret, json_encoded) == sig
360
- rescue ParseError
361
- end
362
-
363
-
364
-
365
-
366
-
367
- # oauth related
368
-
369
- def authorize_url opts={}
370
- query = {:client_id => app_id, :access_token => nil}.merge(opts)
371
- "#{graph_server}oauth/authorize#{build_query_string(query)}"
372
- end
373
-
374
- def authorize! opts={}
375
- query = {:client_id => app_id, :client_secret => secret}.merge(opts)
376
- self.data = Rack::Utils.parse_query(
377
- request({:auto_decode => false}.merge(opts),
378
- [:get, url('oauth/access_token', query)]))
379
- end
380
-
381
-
382
-
383
-
384
-
385
- # old rest facebook api, i will definitely love to remove them someday
386
-
387
- def old_rest path, query={}, opts={}, &cb
388
- request(
389
- opts,
390
- [:get,
391
- url("method/#{path}", {:format => 'json'}.merge(query),
392
- old_server, opts)],
393
- &cb)
394
- end
395
-
396
- def secret_old_rest path, query={}, opts={}, &cb
397
- old_rest(path, query, {:secret => true}.merge(opts), &cb)
398
- end
399
- alias_method :broken_old_rest, :secret_old_rest
400
-
401
- def exchange_sessions query={}, opts={}, &cb
402
- q = {:client_id => app_id, :client_secret => secret,
403
- :type => 'client_cred'}.merge(query)
404
- request(opts, [:post, url('oauth/exchange_sessions', q)], &cb)
405
- end
406
-
407
- def fql code, query={}, opts={}, &cb
408
- old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
409
- end
410
-
411
- def fql_multi codes, query={}, opts={}, &cb
412
- old_rest('fql.multiquery',
413
- {:queries => self.class.json_encode(codes)}.merge(query), opts, &cb)
414
- end
415
-
416
-
417
-
418
-
419
-
420
- private
421
- def request opts, *reqs, &cb
422
- Timeout.timeout(timeout){
423
- if opts[:async]
424
- request_em(opts, reqs, &cb)
425
- else
426
- request_rc(opts, *reqs.first, &cb)
427
- end
428
- }
429
- end
430
-
431
- def request_em opts, reqs
432
- start_time = Time.now
433
- rs = reqs.map{ |(meth, uri, payload)|
434
- r = EM::HttpRequest.new(uri).send(meth, :body => payload)
435
- if cached = cache_get(uri)
436
- # TODO: this is hack!!
437
- r.instance_variable_set('@response', cached)
438
- r.instance_variable_set('@state' , :finish)
439
- r.on_request_complete
440
- r.succeed(r)
441
- else
442
- r.callback{
443
- cache_for(uri, r.response, meth)
444
- log(Event::Requested.new(Time.now - start_time, uri))
445
- }
446
- r.error{
447
- log(Event::Failed.new(Time.now - start_time, uri))
448
- }
449
- end
450
- r
451
- }
452
- EM::MultiRequest.new(rs){ |m|
453
- # TODO: how to deal with the failed?
454
- clients = m.responses[:succeeded]
455
- results = clients.map{ |client|
456
- post_request(client.response, client.uri)
457
- }
458
-
459
- if reqs.size == 1
460
- yield(results.first)
461
- else
462
- log(Event::MultiDone.new(Time.now - start_time,
463
- clients.map(&:uri).join(', ')))
464
- yield(results)
465
- end
466
- }
467
- end
468
-
469
- def request_rc opts, meth, uri, payload=nil, &cb
470
- start_time = Time.now
471
- post_request(cache_get(uri) || fetch(meth, uri, payload), uri, opts, &cb)
472
- rescue RestClient::Exception => e
473
- post_request(e.http_body, uri, opts, &cb)
474
- ensure
475
- log(Event::Requested.new(Time.now - start_time, uri))
476
- end
477
-
478
- def build_query_string query={}, opts={}
479
- token = opts[:secret] ? secret_access_token : access_token
480
- qq = token ? {:access_token => token}.merge(query) : query
481
- q = qq.select{ |k, v| v } # compacting the hash
482
- return '' if q.empty?
483
- return '?' + q.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
484
- end
485
-
486
- def build_headers
487
- headers = {}
488
- headers['Accept'] = accept if accept
489
- headers['Accept-Language'] = lang if lang
490
- headers
491
- end
492
-
493
- def post_request result, uri='', opts={}, &cb
494
- if decode?(opts)
495
- # [this].first is not needed for yajl-ruby
496
- decoded = self.class.json_decode("[#{result}]").first
497
- check_error(decoded, uri, &cb)
498
- else
499
- block_given? ? yield(result) : result
500
- end
501
- rescue ParseError => error
502
- error_handler.call(error, uri) if error_handler
503
- end
504
-
505
- def decode? opts
506
- if opts.has_key?(:auto_decode)
507
- opts[:auto_decode]
508
- elsif opts.has_key?(:suppress_decode)
509
- !opts[:suppress_decode]
510
- else
511
- auto_decode
512
- end
513
- end
514
-
515
- def check_sig_and_return_data cookies
516
- cookies if secret && calculate_sig(cookies) == cookies['sig']
517
- end
518
-
519
- def check_error hash, uri
520
- if error_handler && hash.kind_of?(Hash) &&
521
- (hash['error'] || # from graph api
522
- hash['error_code']) # from fql
523
- error_handler.call(hash, uri)
524
- else
525
- block_given? ? yield(hash) : hash
526
- end
527
- end
528
-
529
- def calculate_sig cookies
530
- Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
531
- end
532
-
533
- def fbs_without_sig cookies
534
- cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
535
- end
536
-
537
- def cache_key uri
538
- Digest::MD5.hexdigest(uri)
539
- end
540
-
541
- def cache_get uri
542
- return unless cache
543
- start_time = Time.now
544
- cache[cache_key(uri)].tap{ |result|
545
- log(Event::CacheHit.new(Time.now - start_time, uri)) if result
546
- }
547
- end
548
-
549
- def cache_for uri, result, meth
550
- cache[cache_key(uri)] = result if cache && meth == :get
551
- end
552
-
553
- def fetch meth, uri, payload
554
- RestClient::Request.execute(:method => meth, :url => uri,
555
- :headers => build_headers,
556
- :payload => payload).body.
557
- tap{ |result| cache_for(uri, result, meth) }
558
- end
559
-
560
- def merge_data lhs, rhs
561
- [lhs, rhs].each{ |hash|
562
- return rhs.reject{ |k, v| k == 'paging' } if
563
- !hash.kind_of?(Hash) || !hash['data'].kind_of?(Array)
564
- }
565
- lhs['data'].unshift(*rhs['data'])
566
- lhs
567
- end
568
-
569
- def log event
570
- log_handler.call(event) if log_handler
571
- log_method .call("DEBUG: #{event}") if log_method
572
- end
573
- end
7
+ require 'rest-graph/rails_util' if Object.const_defined?(:Rails)