rest-core 0.0.1

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