rest-core 0.0.1

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 (65) hide show
  1. data/.gitignore +6 -0
  2. data/.gitmodules +3 -0
  3. data/.travis.yml +9 -0
  4. data/CONTRIBUTORS +11 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +201 -0
  7. data/NOTE.md +48 -0
  8. data/README +83 -0
  9. data/README.md +83 -0
  10. data/Rakefile +26 -0
  11. data/TODO.md +17 -0
  12. data/example/facebook.rb +145 -0
  13. data/example/github.rb +21 -0
  14. data/lib/rest-core.rb +48 -0
  15. data/lib/rest-core/app/ask.rb +11 -0
  16. data/lib/rest-core/app/rest-client.rb +24 -0
  17. data/lib/rest-core/builder.rb +24 -0
  18. data/lib/rest-core/client.rb +278 -0
  19. data/lib/rest-core/client/github.rb +19 -0
  20. data/lib/rest-core/client/linkedin.rb +57 -0
  21. data/lib/rest-core/client/rest-graph.rb +262 -0
  22. data/lib/rest-core/client/twitter.rb +59 -0
  23. data/lib/rest-core/client_oauth1.rb +25 -0
  24. data/lib/rest-core/event.rb +17 -0
  25. data/lib/rest-core/middleware.rb +53 -0
  26. data/lib/rest-core/middleware/cache.rb +80 -0
  27. data/lib/rest-core/middleware/common_logger.rb +27 -0
  28. data/lib/rest-core/middleware/default_headers.rb +11 -0
  29. data/lib/rest-core/middleware/default_query.rb +11 -0
  30. data/lib/rest-core/middleware/default_site.rb +15 -0
  31. data/lib/rest-core/middleware/defaults.rb +44 -0
  32. data/lib/rest-core/middleware/error_detector.rb +16 -0
  33. data/lib/rest-core/middleware/error_detector_http.rb +11 -0
  34. data/lib/rest-core/middleware/error_handler.rb +19 -0
  35. data/lib/rest-core/middleware/json_decode.rb +83 -0
  36. data/lib/rest-core/middleware/oauth1_header.rb +81 -0
  37. data/lib/rest-core/middleware/oauth2_query.rb +19 -0
  38. data/lib/rest-core/middleware/timeout.rb +13 -0
  39. data/lib/rest-core/util/hmac.rb +22 -0
  40. data/lib/rest-core/version.rb +4 -0
  41. data/lib/rest-core/wrapper.rb +55 -0
  42. data/lib/rest-graph/config_util.rb +43 -0
  43. data/rest-core.gemspec +162 -0
  44. data/task/.gitignore +1 -0
  45. data/task/gemgem.rb +184 -0
  46. data/test/common.rb +29 -0
  47. data/test/config/rest-graph.yaml +7 -0
  48. data/test/pending/test_load_config.rb +42 -0
  49. data/test/pending/test_multi.rb +123 -0
  50. data/test/pending/test_test_util.rb +86 -0
  51. data/test/test_api.rb +98 -0
  52. data/test/test_cache.rb +62 -0
  53. data/test/test_default.rb +27 -0
  54. data/test/test_error.rb +66 -0
  55. data/test/test_handler.rb +87 -0
  56. data/test/test_misc.rb +75 -0
  57. data/test/test_oauth.rb +42 -0
  58. data/test/test_oauth1_header.rb +46 -0
  59. data/test/test_old.rb +116 -0
  60. data/test/test_page.rb +110 -0
  61. data/test/test_parse.rb +131 -0
  62. data/test/test_rest-graph.rb +10 -0
  63. data/test/test_serialize.rb +44 -0
  64. data/test/test_timeout.rb +25 -0
  65. metadata +267 -0
@@ -0,0 +1,19 @@
1
+
2
+ RestCore::Builder.client('Github') do
3
+ s = self.class # this is only for ruby 1.8!
4
+ use s::Timeout , 10
5
+
6
+ use s::DefaultSite , 'https://api.github.com/'
7
+ use s::DefaultHeaders, {'Accept' => 'application/json'}
8
+ use s::Oauth2Query , 'access_token', nil
9
+
10
+ use s::CommonLogger , lambda{|obj|obj}
11
+ use s::Cache , {}, nil do
12
+ use s::ErrorHandler , lambda{|env| raise env[s::RESPONSE_BODY]['message']}
13
+ use s::ErrorDetectorHttp
14
+ use s::JsonDecode , true
15
+ run s::Ask
16
+ end
17
+
18
+ run s::RestClient
19
+ end
@@ -0,0 +1,57 @@
1
+
2
+ RestCore::Builder.client('Linkedin', :data) do
3
+ s = self.class # this is only for ruby 1.8!
4
+ use s::Timeout , 10
5
+
6
+ use s::DefaultSite , 'https://api.linkedin.com/'
7
+ use s::DefaultHeaders, {'Accept' => 'application/json'}
8
+ use s::DefaultQuery , {'format' => 'json'}
9
+
10
+ use s::Oauth1Header ,
11
+ 'uas/oauth/requestToken', 'uas/oauth/accessToken',
12
+ 'https://www.linkedin.com/uas/oauth/authorize'
13
+
14
+ use s::CommonLogger , method(:puts)
15
+
16
+ use s::Cache , {}, nil do
17
+ use s::ErrorHandler , lambda{|env|
18
+ if (body = env[s::RESPONSE_BODY]).kind_of?(Hash)
19
+ raise body['message']
20
+ else
21
+ raise body
22
+ end
23
+ }
24
+ use s::ErrorDetectorHttp
25
+ use s::JsonDecode , true
26
+ run s::Ask
27
+ end
28
+
29
+ use s::Defaults , :data => lambda{{}}
30
+
31
+ run s::RestClient
32
+ end
33
+
34
+ module Linkedin::Client
35
+ include RestCore
36
+
37
+ def oauth_token
38
+ data['oauth_token'] if data.kind_of?(Hash)
39
+ end
40
+ def oauth_token= token
41
+ data['oauth_token'] = token if data.kind_of?(Hash)
42
+ end
43
+ def oauth_token_secret
44
+ data['oauth_token_secret'] if data.kind_of?(Hash)
45
+ end
46
+ def oauth_token_secret= secret
47
+ data['oauth_token_secret'] = secret if data.kind_of?(Hash)
48
+ end
49
+
50
+ private
51
+ def set_token query
52
+ self.data = query
53
+ end
54
+ end
55
+
56
+ Linkedin.send(:include, RestCore::ClientOauth1)
57
+ Linkedin.send(:include, Linkedin::Client)
@@ -0,0 +1,262 @@
1
+
2
+ require 'rest-core'
3
+ require 'rest-core/util/hmac'
4
+
5
+ # optional gem
6
+ begin; require 'rack'; rescue LoadError; end
7
+
8
+ RestCore::Builder.client('RestGraph', :data, :app_id, :secret, :old_site) do
9
+ s = self.class # this is only for ruby 1.8!
10
+ use s::Timeout , 10
11
+
12
+ use s::DefaultSite , 'https://graph.facebook.com/'
13
+ use s::DefaultHeaders, {'Accept' => 'application/json',
14
+ 'Accept-Language' => 'en-us'}
15
+ use s::Oauth2Query , 'access_token', nil
16
+
17
+ use s::CommonLogger , lambda{|obj|obj}
18
+
19
+ use s::Cache , {}, nil do
20
+ use s::ErrorHandler , lambda{ |env| raise ::RestGraph::Error.call(env) }
21
+ use s::ErrorDetector , lambda{ |env|
22
+ if env[s::RESPONSE_BODY].kind_of?(Hash)
23
+ env[s::RESPONSE_BODY]['error'] ||
24
+ env[s::RESPONSE_BODY]['error_code']
25
+ end}
26
+
27
+ use s::JsonDecode , true
28
+ run s::Ask
29
+ end
30
+
31
+ use s::Defaults , :data => lambda{{}},
32
+ :old_site => 'https://api.facebook.com/'
33
+
34
+ run s::RestClient
35
+ end
36
+
37
+ class RestGraph::Error < RuntimeError
38
+ include RestCore
39
+ class AccessToken < RestGraph::Error; end
40
+ class InvalidAccessToken < AccessToken ; end
41
+ class MissingAccessToken < AccessToken ; end
42
+
43
+ attr_reader :error, :url
44
+ def initialize error, url=''
45
+ @error, @url = error, url
46
+ super("#{error.inspect} from #{url}")
47
+ end
48
+
49
+ def self.call env
50
+ error, url = env[RESPONSE_BODY], Middleware.request_uri(env)
51
+ return new(env[FAIL], url) unless error.kind_of?(Hash)
52
+ if invalid_token?(error)
53
+ InvalidAccessToken.new(error, url)
54
+ elsif missing_token?(error)
55
+ MissingAccessToken.new(error, url)
56
+ else
57
+ new(error, url)
58
+ end
59
+ end
60
+
61
+ def self.invalid_token? error
62
+ (%w[OAuthInvalidTokenException
63
+ OAuthException].include?((error['error'] || {})['type'])) ||
64
+ (error['error_code'] == 190) # Invalid OAuth 2.0 Access Token
65
+ end
66
+
67
+ def self.missing_token? error
68
+ (error['error'] || {})['message'] =~ /^An active access token/ ||
69
+ (error['error_code'] == 104) # Requires valid signature
70
+ end
71
+ end
72
+
73
+ module RestGraph::Client
74
+ include RestCore
75
+
76
+ def self.included mod
77
+ mod.send(:alias_method, :auto_decode , :json_decode )
78
+ mod.send(:alias_method, :auto_decode=, :json_decode=)
79
+ end
80
+
81
+ def oauth_token
82
+ data['access_token'] || data['oauth_token'] if data.kind_of?(Hash)
83
+ end
84
+ def oauth_token= token
85
+ data['access_token'] = token if data.kind_of?(Hash)
86
+ end
87
+ alias_method :access_token , :oauth_token
88
+ alias_method :access_token=, :oauth_token=
89
+
90
+ def secret_oauth_token ; "#{app_id}|#{secret}" ; end
91
+ alias_method :secret_access_token, :secret_oauth_token
92
+
93
+ def accept ; headers['Accept'] ; end
94
+ def accept= val; headers['Accept'] = val; end
95
+ def lang ; headers['Accept-Language'] ; end
96
+ def lang= val; headers['Accept-Language'] = val; end
97
+
98
+ def authorized? ; !!oauth_token ; end
99
+
100
+ def next_page hash, opts={}, &cb
101
+ if hash['paging'].kind_of?(Hash) && hash['paging']['next']
102
+ request(opts, [:get, URI.encode(hash['paging']['next'])], &cb)
103
+ else
104
+ yield(nil) if block_given?
105
+ end
106
+ end
107
+
108
+ def prev_page hash, opts={}, &cb
109
+ if hash['paging'].kind_of?(Hash) && hash['paging']['previous']
110
+ request(opts, [:get, URI.encode(hash['paging']['previous'])], &cb)
111
+ else
112
+ yield(nil) if block_given?
113
+ end
114
+ end
115
+ alias_method :previous_page, :prev_page
116
+
117
+ def for_pages hash, pages=1, opts={}, kind=:next_page, &cb
118
+ if pages > 1
119
+ merge_data(send(kind, hash, opts){ |result|
120
+ yield(result.freeze) if block_given?
121
+ for_pages(result, pages - 1, opts, kind, &cb) if result
122
+ }, hash)
123
+ else
124
+ yield(nil) if block_given?
125
+ hash
126
+ end
127
+ end
128
+
129
+ # cookies, app_id, secrect related below
130
+
131
+ def parse_rack_env! env
132
+ env['HTTP_COOKIE'].to_s =~ /fbs_#{app_id}=([^\;]+)/
133
+ self.data = parse_fbs!($1)
134
+ end
135
+
136
+ def parse_cookies! cookies
137
+ self.data = parse_fbs!(cookies["fbs_#{app_id}"])
138
+ end
139
+
140
+ def parse_fbs! fbs
141
+ self.data = check_sig_and_return_data(
142
+ # take out facebook sometimes there but sometimes not quotes in cookies
143
+ Rack::Utils.parse_query(fbs.to_s.sub(/^"/, '').sub(/"$/, '')))
144
+ end
145
+
146
+ def parse_json! json
147
+ self.data = json &&
148
+ check_sig_and_return_data(JsonDecode.json_decode(json))
149
+ rescue JsonDecode::ParseError
150
+ self.data = nil
151
+ end
152
+
153
+ def fbs
154
+ "#{fbs_without_sig(data).join('&')}&sig=#{calculate_sig(data)}"
155
+ end
156
+
157
+ # facebook's new signed_request...
158
+
159
+ def parse_signed_request! request
160
+ sig_encoded, json_encoded = request.split('.')
161
+ sig, json = [sig_encoded, json_encoded].map{ |str|
162
+ "#{str.tr('-_', '+/')}==".unpack('m').first
163
+ }
164
+ self.data = check_sig_and_return_data(
165
+ JsonDecode.json_decode(json).merge('sig' => sig)){
166
+ Hmac.sha256(secret, json_encoded)
167
+ }
168
+ rescue JsonDecode::ParseError
169
+ self.data = nil
170
+ end
171
+
172
+ # oauth related
173
+
174
+ def authorize_url opts={}
175
+ url('oauth/authorize',
176
+ {:client_id => app_id, :access_token => nil}.merge(opts))
177
+ end
178
+
179
+ def authorize! opts={}
180
+ query = {:client_id => app_id, :client_secret => secret}.merge(opts)
181
+ self.data = Rack::Utils.parse_query(
182
+ request({:auto_decode => false}.merge(opts),
183
+ [:get, url('oauth/access_token', query)]))
184
+ end
185
+
186
+ # old rest facebook api, i will definitely love to remove them someday
187
+
188
+ def old_rest path, query={}, opts={}, &cb
189
+ uri = url("method/#{path}", {:format => 'json'}.merge(query),
190
+ {:site => old_site}.merge(opts))
191
+ if opts[:post]
192
+ request(
193
+ opts.merge('cache.key' => uri, 'cache.post' => true),
194
+ [:post,
195
+ url("method/#{path}", {:format => 'json'},
196
+ {:site => old_site}.merge(opts)),
197
+ {}, query],
198
+ &cb)
199
+ else
200
+ request(opts, [:get, uri], &cb)
201
+ end
202
+ end
203
+
204
+ def secret_old_rest path, query={}, opts={}, &cb
205
+ old_rest(path, query, {:secret => true}.merge(opts), &cb)
206
+ end
207
+
208
+ def fql code, query={}, opts={}, &cb
209
+ old_rest('fql.query', {:query => code}.merge(query), opts, &cb)
210
+ end
211
+
212
+ def fql_multi codes, query={}, opts={}, &cb
213
+ old_rest('fql.multiquery',
214
+ {:queries => JsonDecode.json_encode(codes)}.merge(query), opts, &cb)
215
+ end
216
+
217
+ def exchange_sessions query={}, opts={}, &cb
218
+ q = {:client_id => app_id, :client_secret => secret,
219
+ :type => 'client_cred'}.merge(query)
220
+ request(opts, [:post, url('oauth/exchange_sessions', q)], &cb)
221
+ end
222
+
223
+ protected
224
+ def build_env env={}
225
+ super(env.inject({}){ |r, (k, v)|
226
+ case k.to_s
227
+ when 'auto_decode'; r['json_decode' ] = v
228
+ when 'secret' ; r['oauth_token' ] = secret_oauth_token
229
+ when 'cache' ; r['cache.update'] = !!!v
230
+ else ; r[k.to_s] = v
231
+ end
232
+ r
233
+ })
234
+ end
235
+
236
+ def check_sig_and_return_data cookies
237
+ cookies if secret && if block_given?
238
+ yield
239
+ else
240
+ calculate_sig(cookies)
241
+ end == cookies['sig']
242
+ end
243
+
244
+ def calculate_sig cookies
245
+ Digest::MD5.hexdigest(fbs_without_sig(cookies).join + secret)
246
+ end
247
+
248
+ def fbs_without_sig cookies
249
+ cookies.reject{ |(k, v)| k == 'sig' }.sort.map{ |a| a.join('=') }
250
+ end
251
+
252
+ def merge_data lhs, rhs
253
+ [lhs, rhs].each{ |hash|
254
+ return rhs.reject{ |k, v| k == 'paging' } if
255
+ !hash.kind_of?(Hash) || !hash['data'].kind_of?(Array)
256
+ }
257
+ lhs['data'].unshift(*rhs['data'])
258
+ lhs
259
+ end
260
+ end
261
+
262
+ RestGraph.send(:include, RestGraph::Client)
@@ -0,0 +1,59 @@
1
+
2
+ RestCore::Builder.client('Twitter', :data) do
3
+ s = self.class # this is only for ruby 1.8!
4
+ use s::Timeout , 10
5
+
6
+ use s::DefaultSite , 'https://api.twitter.com/'
7
+ use s::DefaultHeaders, {'Accept' => 'application/json'}
8
+
9
+ use s::Oauth1Header ,
10
+ 'oauth/request_token', 'oauth/access_token', 'oauth/authorize'
11
+
12
+ use s::CommonLogger , method(:puts)
13
+
14
+ use s::Cache , {}, nil do
15
+ use s::ErrorHandler , lambda{ |env|
16
+ if (body = env[s::RESPONSE_BODY]).kind_of?(Hash)
17
+ raise body['error']
18
+ else
19
+ raise body
20
+ end
21
+ }
22
+ use s::ErrorDetectorHttp
23
+ use s::JsonDecode , true
24
+ run s::Ask
25
+ end
26
+
27
+ use s::Defaults , :data => lambda{{}}
28
+
29
+ run s::RestClient
30
+ end
31
+
32
+ module Twitter::Client
33
+ include RestCore
34
+
35
+ def oauth_token
36
+ data['oauth_token'] if data.kind_of?(Hash)
37
+ end
38
+ def oauth_token= token
39
+ data['oauth_token'] = token if data.kind_of?(Hash)
40
+ end
41
+ def oauth_token_secret
42
+ data['oauth_token_secret'] if data.kind_of?(Hash)
43
+ end
44
+ def oauth_token_secret= secret
45
+ data['oauth_token_secret'] = secret if data.kind_of?(Hash)
46
+ end
47
+
48
+ def tweet status, opt={}
49
+ post('1/statuses/update.json', {:status => status}.merge(opt))
50
+ end
51
+
52
+ private
53
+ def set_token query
54
+ self.data = query
55
+ end
56
+ end
57
+
58
+ Twitter.send(:include, RestCore::ClientOauth1)
59
+ Twitter.send(:include, Twitter::Client)
@@ -0,0 +1,25 @@
1
+
2
+ require 'rack'
3
+
4
+ module RestCore::ClientOauth1
5
+ include RestCore
6
+
7
+ def authorize_url!
8
+ set_token(Rack::Utils.parse_query(
9
+ post(request_token_path, {}, {}, {:json_decode => false})))
10
+
11
+ url(authorize_path, :oauth_token => oauth_token, :format => false)
12
+ end
13
+
14
+ def authorize! verifier
15
+ set_token(Rack::Utils.parse_query(
16
+ post(access_token_path, {}, {}, {:verifier => verifier,
17
+ :json_decode => false})))
18
+ end
19
+
20
+ private
21
+ def set_token query
22
+ self.oauth_token = query['oauth_token']
23
+ self.oauth_token_secret = query['oauth_token_secret']
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module RestCore
3
+ EventStruct = Struct.new(:duration, :message) unless
4
+ RestCore.const_defined?(:EventStruct)
5
+
6
+ class Event < EventStruct
7
+ # self.class.name[/(?<=::)\w+$/] if RUBY_VERSION >= '1.9.2'
8
+ def name; self.class.name[/::(\w+)$/, 1] ; end
9
+ def to_s; "spent #{duration} #{name} #{message}"; end
10
+ end
11
+ class Event::MultiDone < Event; end
12
+ class Event::Requested < Event; end
13
+ class Event::CacheHit < Event; end
14
+ class Event::CacheCleared < Event; end
15
+ class Event::Failed < Event; end
16
+ class Event::WithHeader < Event; end
17
+ end
@@ -0,0 +1,53 @@
1
+
2
+ require 'rest-core'
3
+
4
+ require 'cgi'
5
+
6
+ module RestCore::Middleware
7
+ include RestCore
8
+
9
+ def self.included mod
10
+ mod.send(:include, RestCore)
11
+ mod.send(:attr_reader, :app)
12
+ return unless mod.respond_to?(:members)
13
+ src = mod.members.map{ |member| <<-RUBY }
14
+ def #{member} env
15
+ if env.key?('#{member}')
16
+ env['#{member}']
17
+ else
18
+ @#{member}
19
+ end
20
+ end
21
+ RUBY
22
+ args = [:app] + mod.members
23
+ para_list = args.map{ |a| "#{a}=nil"}.join(', ')
24
+ args_list = args .join(', ')
25
+ ivar_list = args.map{ |a| "@#{a}" }.join(', ')
26
+ src << <<-RUBY
27
+ def initialize #{para_list}
28
+ #{ivar_list} = #{args_list}
29
+ end
30
+ self
31
+ RUBY
32
+ accessor = Module.new.module_eval(src.join("\n"), __FILE__, __LINE__)
33
+ mod.const_set(:Accessor, accessor)
34
+ mod.send(:include, accessor)
35
+ end
36
+
37
+ def call env ; app.call(env) ; end
38
+ def fail env, obj; env.merge(FAIL => (env[FAIL] || []) + [obj]); end
39
+ def log env, obj; env.merge(LOG => (env[LOG] || []) + [obj]); end
40
+
41
+ module_function
42
+ def request_uri env
43
+ # compacting the hash
44
+ if (query = (env[REQUEST_QUERY] || {}).select{ |k, v| v }).empty?
45
+ env[REQUEST_PATH].to_s
46
+ else
47
+ q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
48
+ "#{env[REQUEST_PATH]}#{q}" \
49
+ "#{query.map{ |(k, v)| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')}"
50
+ end
51
+ end
52
+ public :request_uri
53
+ end