rgdata 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,14 @@
1
1
  = RGData
2
2
 
3
3
  RGData makes it easy to get data from Google's Data APIs.
4
- It streamlines requests and gives you back good ole Ruby objects.
4
+ It streamlines the parts that give you headaches (requests, OAUTH2 authentication, etc.) and gives you back good ole Ruby objects.
5
5
 
6
6
  An Example:
7
7
 
8
8
  domain = 'redningja.com'
9
9
  profiles = RGData::API::Profiles.retrieve domain, oauth_token: 'access_token_thatlookslikegibberish'
10
- => [#<RGData::Model::Profile full_name: 'Jared Ning'>, ...]
11
- # Now you can just do this:
10
+ #=> [#<RGData::Model::Profile full_name: 'Jared Ning'>, ...]
11
+ # With a Ruby object in hand, getting the data is easy:
12
12
  profiles.first.full_name
13
13
  # Want each user's email addresses?
14
14
  profiles.map(&:emails)
@@ -16,7 +16,7 @@ An Example:
16
16
  == Installation
17
17
 
18
18
  gem install rgdata
19
-
19
+
20
20
  RGData.config do
21
21
  client_id <your client_id>
22
22
  client_secret <your client_secret>
@@ -30,30 +30,33 @@ There's a lot of setup, but after that it should be easy.
30
30
  * Setup
31
31
  * Get your client_id and client_secret
32
32
  * https://code.google.com/apis/console
33
- * Click on Identity at the left.
33
+ * Click on 'API Access' at the left.
34
34
  * Look for OAuth2 Credentials and you'll find your client_id and client_secret
35
- * While you're you may want to list your redirect URIs if you know what they will be. These must match your authentication requests (more on this later).
35
+ * While you're here, you may want to list your redirect URIs if you know what they will be. These must match your authentication requests (more on this in just a bit).
36
36
  * Request authentication. (Depending on which API you are using, there may be specific instructions you may want to tell your user about. See the individual APIs classes for specifics.)
37
- * Your user will go to the request URL generated for you by calling, for example, RGData::API::Profiles.request_url(scope, redirect_uri).
37
+ * Your user will go to the request URL generated for you by calling, for example, RGData::API::Profiles.request_url(redirect_uri) # redirect_uri MUST BE LISTED IN YOUR API CONSOLE.
38
38
  * They will be promted by Google to authenticate and grant permission.
39
- * Successful authentication will return hash that you are responsible for collecting. It includes 3 parts:
39
+ * Successful authentication will return an authorization code in the form of a callback to redirect_uri. The authorization code is attached to the URL as a parameter - e.g. http://redirect_back.com?authorization_code=42istheanswer.
40
+ * Now you have the dough, so go get the bread. (You get 1 shot at this. Once the authorization_code is used on Google, it's useless.) Use this authorization code to swap for a hash with all the goodies. The hash has 3 parts:
40
41
  * access_token: This is what we're after. You can use this immediately to retrieve data. You can also get one by using refresh_token which we'll get to in a sec.
41
- * expiration: You have this many seconds to use access_token before it expires.
42
+ * expires_in: You have this many seconds to use access_token before it expires.
42
43
  * refresh_token: See below.
43
- * Once you have your access token, you can start accessing the data.
44
+ * Finalyy, once you have your access token, you can start accessing the data.
44
45
 
45
46
  === Refresh tokens
46
47
 
47
- A refresh token won't get you into the data, but it will allow you to get more access tokens in the future. Keep this if you don't want your users to re-authenticate over and over again.
48
+ A refresh token won't get you into the data, but it will allow you to get more access tokens in the future.
49
+ Keep this if you don't want your users to re-authenticate over and over again.
50
+ REFRESH TOKENS EXPIRE AFTER 24 HOURS.
48
51
  To get another access token:
49
52
 
50
- RGData::API::Profiles.get_access_token(refresh_token)
53
+ RGData::API::Profiles.swap_refresh_token(refresh_token)
51
54
 
52
55
  For convenience, APIs are smart enough to do this work for you:
53
56
 
54
57
  RGData::API::Profiles.retrieve domain, refresh_token: refresh_token
55
58
 
56
- === client-side and native application authentication
59
+ === Client-side and native application authentication
57
60
 
58
61
  TODO
59
62
 
@@ -86,6 +89,29 @@ Please feel free to contribute more APIs.
86
89
  Take a look at lib/rgdata/apis/profiles.rb and lib/rgdata/models/profile.rb to see how easy it is.
87
90
  For the API, just find the scope (link below)
88
91
 
92
+ == TODOs
93
+
94
+ Feel free to contribute.
95
+
96
+ * Client-side authentication
97
+ * Native application authentication
98
+ * Retrieving data with query parameters (limit, start index, etc.)
99
+ * Retrieving individual (as opposed to all)
100
+ * Lots of more APIs.
101
+ * Capture error messages: If google denies something, there may be some useful error messages.
102
+
103
+ == Testing
104
+
105
+ This gem tests live with Google.
106
+ You'll need to set some environment variables for your own client_id, client_secret, refresh_token, and domain.
107
+ See mspec/spec_helper.rb.
108
+ I set them in my .bash_profile.
109
+
110
+ export GOOGLE_APPS_CLIENT_ID='1234567890.apps.googleusercontent.com'
111
+ export GOOGLE_APPS_CLIENT_SECRET='blahblahblah'
112
+ export GOOGLE_APPS_REFRESH_TOKEN='sierra_mist_foobar'
113
+ export GOOGLE_APPS_DOMAIN='redningja.com'
114
+
89
115
  == Compatibility
90
116
 
91
117
  Ruby 1.9. Want 1.8 support? Go fork yourself.
@@ -18,21 +18,30 @@ module RGData::API
18
18
  RGData::Authentication.request_url(scope, redirect_uri)
19
19
  end
20
20
 
21
- # Send request and return response.
21
+ # Send request for data (not authentication) and return response.
22
22
  # options can be :oauth_token, :access_token, or :refresh_token.
23
23
  def do_request(domain, options)
24
24
  oauth_token = extract_oauth_token_or_access_token_or_get_one_using_refresh_token(options)
25
- query = {oauth_token: oauth_token}
26
- query.verify_keys_present_and_values_not_nil(:oauth_token)
25
+ query = {
26
+ oauth_token: oauth_token,
27
+ }
28
+ query.verify_keys_present_and_values_not_blank(:oauth_token)
29
+ raise 'Domain cannot be blank' if domain.blank?
27
30
  get "/#{domain}/full", query: query
28
31
  end
29
32
 
30
- # Perform request and instantiate new model objects.
33
+ # Perform request and create new model objects.
34
+ # See do_request() for arguments info.
31
35
  def retrieve(domain, options)
36
+ raise 'domain cannot be nil' unless domain
32
37
  full_xml_hash = do_request(domain, options)
33
38
  full_xml_hash['feed']['entry'].map do |xml_hash|
34
39
  model.new_from_xml_hash xml_hash
35
40
  end
41
+ rescue
42
+ puts 'Unexpected response:'
43
+ puts full_xml_hash
44
+ raise
36
45
  end
37
46
 
38
47
  private
@@ -45,7 +54,10 @@ module RGData::API
45
54
  oauth_token = hash.extract_values_at(:oauth_token, :access_token).compact.first
46
55
  unless oauth_token
47
56
  refresh_token = hash.extract(:refresh_token)
48
- oauth_token = RGData::Authentication::get_access_token(refresh_token)
57
+ raise "Refresh token cannot be blank" if refresh_token.blank?
58
+ response_hash = RGData::Authentication::swap_refresh_token(refresh_token)
59
+ oauth_token = response_hash['access_token']
60
+ raise "Unexpected response: #{response_hash}" unless oauth_token
49
61
  end
50
62
  oauth_token
51
63
  end
@@ -10,26 +10,37 @@ class RGData::Authentication
10
10
  method = Net::HTTP::Get
11
11
  path = default_options[:base_uri] + '/auth'
12
12
  query = {
13
+ client_id: RGData.config.client_id,
13
14
  redirect_uri: redirect_uri,
14
15
  scope: scope,
15
16
  response_type: 'code',
16
- client_id: RGData.config.client_id,
17
17
  }
18
- query.verify_keys_present_and_values_not_nil(:client_id, :redirect_uri, :scope, :response_type)
18
+ query.verify_keys_present_and_values_not_blank(:client_id, :redirect_uri, :scope, :response_type)
19
19
  request = HTTParty::Request.new(method, path, query: query)
20
20
  URI.unescape(request.uri.to_s)
21
21
  end
22
22
 
23
- def get_access_token(refresh_token)
23
+ def swap_authorization_code(authorization_code, redirect_uri)
24
+ body = {
25
+ client_id: RGData.config.client_id,
26
+ client_secret: RGData.config.client_secret,
27
+ code: authorization_code,
28
+ grant_type: 'authorization_code',
29
+ redirect_uri: redirect_uri
30
+ }
31
+ body.verify_keys_present_and_values_not_blank(:client_secret, :client_id, :code, :grant_type, :redirect_uri)
32
+ post('/token', body: body)
33
+ end
34
+
35
+ def swap_refresh_token(refresh_token)
24
36
  body = {
25
- refresh_token: refresh_token,
26
- grant_type: 'refresh_token',
27
- client_id: RGData.config.client_id,
28
- client_secret: RGData.config.client_secret,
37
+ client_id: RGData.config.client_id,
38
+ client_secret: RGData.config.client_secret,
39
+ refresh_token: refresh_token,
40
+ grant_type: 'refresh_token',
29
41
  }
30
- body.verify_keys_present_and_values_not_nil(:refresh_token, :grant_type, :client_secret, :client_id)
31
- response = post('/token', body: body)
32
- response['access_token']
42
+ body.verify_keys_present_and_values_not_blank(:refresh_token, :grant_type, :client_secret, :client_id)
43
+ post('/token', body: body)
33
44
  end
34
45
 
35
46
  end
@@ -1,3 +1,3 @@
1
1
  module Rgdata
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -3,12 +3,12 @@ class Hash
3
3
  # This gem was meant to help deal with the strictness of APIs
4
4
  # and their inability to give good feedback for mundane detail errors.
5
5
  # This method helps eliminate guessing when it comes to Request query/body/etc. hashes.
6
- def verify_keys_present_and_values_not_nil(*keys)
6
+ def verify_keys_present_and_values_not_blank(*keys)
7
7
  keys.each do |key|
8
8
  unless has_key?(key.to_sym) || has_key?(key.to_s)
9
9
  raise "Options missing '#{key}'"
10
10
  end
11
- if extract(key).nil?
11
+ if extract(key).blank?
12
12
  raise "Option '#{key}' cannot be nil"
13
13
  end
14
14
  end
@@ -0,0 +1,7 @@
1
+ class Object
2
+
3
+ def blank?
4
+ nil? || self == ''
5
+ end
6
+
7
+ end
@@ -9,14 +9,12 @@ describe RGData::API::Profiles do
9
9
  it 'requests authentication url' do
10
10
  redirect_uri = 'http://come.back.here.after.successful.authentication.com/'
11
11
  request_url = RGData::API::Profiles.request_url(redirect_uri)
12
- uri = URI.escape("https://accounts.google.com/o/oauth2/auth?redirect_uri=http://come.back.here.after.successful.authentication.com/&scope=http://www.google.com/m8/feeds/&response_type=code&client_id=#{RGData.config.client_id}")
12
+ uri = URI.escape("https://accounts.google.com/o/oauth2/auth?client_id=#{RGData.config.client_id}&redirect_uri=http://come.back.here.after.successful.authentication.com/&scope=http://www.google.com/m8/feeds/&response_type=code")
13
13
  request_url.must_equal uri
14
14
  end
15
15
 
16
16
  it 'retrieves profiles from a domain' do
17
- domain = 'redningja.com'
18
- access_token = RGData::Authentication.get_access_token(TEST_REFRESH_TOKEN)
19
- profiles = RGData::API::Profiles.retrieve(domain, oauth_token: access_token)
17
+ profiles = RGData::API::Profiles.retrieve(TEST_DOMAIN, refresh_token: TEST_REFRESH_TOKEN)
20
18
  profiles.wont_be_empty
21
19
  end
22
20
 
@@ -0,0 +1,15 @@
1
+ module MiniTest::Assertions
2
+
3
+ def assert_has_keys(hash, keys, msg = nil)
4
+ missing_keys = keys.select { |key| !hash.has_key?(key) }
5
+ assert hash.is_a?(Hash), msg || "Exepcted Hash, got: #{mu_pp(hash)}"
6
+ assert missing_keys.none?, msg || "Hash missing keys: #{missing_keys.join(',')}"
7
+ end
8
+ Object.infect_an_assertion :assert_has_keys, :must_have_keys, true
9
+
10
+ def assert_httparty_response_has_keys(response, keys, msg = nil)
11
+ assert_has_keys response, keys, msg || "Unexpected response: #{response}"
12
+ end
13
+ Object.infect_an_assertion :assert_httparty_response_has_keys, :must_have_response_keys, true
14
+
15
+ end
@@ -6,12 +6,24 @@ describe RGData::Authentication do
6
6
  scope = RGData::API::Profiles.scope
7
7
  redirect_uri = 'http://come.back.here.after.successful.authentication.com/'
8
8
  request_url = RGData::Authentication.request_url(scope, redirect_uri)
9
- uri = URI.escape("https://accounts.google.com/o/oauth2/auth?redirect_uri=http://come.back.here.after.successful.authentication.com/&scope=http://www.google.com/m8/feeds/&response_type=code&client_id=#{RGData.config.client_id}")
9
+ uri = URI.escape("https://accounts.google.com/o/oauth2/auth?client_id=#{RGData.config.client_id}&redirect_uri=http://come.back.here.after.successful.authentication.com/&scope=http://www.google.com/m8/feeds/&response_type=code")
10
10
  request_url.must_equal uri
11
11
  end
12
12
 
13
- it 'retrieves another access_token given a refresh_token' do
14
- RGData::Authentication.get_access_token(TEST_REFRESH_TOKEN).wont_be_nil
13
+ it 'swaps a refresh_token for a hash with access_token and expiration' do
14
+ response = RGData::Authentication.swap_refresh_token(TEST_REFRESH_TOKEN)
15
+ response.must_have_response_keys ['access_token', 'expires_in']
16
+ end
17
+
18
+ # This must be tested live because you have to contact Google through a web interface to get an authorization code.
19
+ # Remember, you only get 1 shot with this authorization code. Once it is sent to Google, it's expired.
20
+ # I setup a Sinatra app that makes it easy to request authorization codes. I will include it later.
21
+ it 'swaps an authorization_code for a hash with access_token, refresh_token, and expiration' do
22
+ unless TEST_AUTHORIZATION_CODE
23
+ skip 'This must be tested with a valid authorization code and registered redirect_uri.'
24
+ end
25
+ response = RGData::Authentication.swap_authorization_code(TEST_AUTHORIZATION_CODE, TEST_REDIRECT_URI)
26
+ response.must_have_response_keys ['access_token', 'expires_in', 'refresh_token']
15
27
  end
16
28
 
17
29
  end
@@ -0,0 +1,9 @@
1
+ module MiniTest::Spec::Helpers
2
+
3
+ def fixture_file(file_name)
4
+ File.open(ROOT_DIR + 'mspec/fixtures/' + file_name)
5
+ end
6
+
7
+ end
8
+
9
+ MiniTest::Unit::TestCase.send :include, MiniTest::Spec::Helpers
@@ -1,23 +1,26 @@
1
1
  require 'minitest/pride'
2
2
  require 'minitest/autorun'
3
+ require 'awesome_print'
4
+
3
5
  require 'pathname'
4
6
  ROOT_DIR = Pathname(File.dirname(File.expand_path(__FILE__))) + '..'
5
7
  $LOAD_PATH << (ROOT_DIR + 'lib').to_s
8
+
6
9
  require 'rgdata'
7
10
  require 'crack'
8
11
 
12
+ $LOAD_PATH << (ROOT_DIR + 'mspec').to_s
13
+ require 'helpers'
14
+ require 'assertions'
15
+
16
+ # Set these in your environment.
9
17
  RGData.config do
10
18
  client_id ENV['GOOGLE_APPS_CLIENT_ID']
11
19
  client_secret ENV['GOOGLE_APPS_CLIENT_SECRET']
12
20
  end
13
21
  TEST_REFRESH_TOKEN = ENV['GOOGLE_APPS_REFRESH_TOKEN']
14
-
15
- module MiniTest::Spec::Helpers
16
-
17
- def fixture_file(file_name)
18
- File.open(ROOT_DIR + 'mspec/fixtures/' + file_name)
19
- end
20
-
21
- end
22
-
23
- MiniTest::Unit::TestCase.send :include, MiniTest::Spec::Helpers
22
+ TEST_DOMAIN = ENV['GOOGLE_APPS_DOMAIN']
23
+ TEST_REDIRECT_URI = ENV['GOOGLE_REDIRECT_URI'] # THIS URI HAS TO BE REGISTERED WITH GOOGLE.
24
+ # Test with authorization code? (See authorization_spec.rb test for more info on testing this.)
25
+ authorization_code_arg = ARGV.detect { |arg| arg.start_with?('AUTHORIZATION_CODE') }
26
+ TEST_AUTHORIZATION_CODE = authorization_code_arg ? /AUTHORIZATION_CODE=(?<code>.*)/.match(authorization_code_arg)[:code] : nil
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rgdata
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.0
5
+ version: 0.2.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jared Ning
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-01 00:00:00 -05:00
13
+ date: 2011-04-10 00:00:00 -05:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -59,12 +59,15 @@ files:
59
59
  - lib/rgdata/models/profile.rb
60
60
  - lib/rgdata/version.rb
61
61
  - lib/support/hash.rb
62
+ - lib/support/object.rb
62
63
  - mspec/api_spec.rb
63
64
  - mspec/apis/api_spec.rb
64
65
  - mspec/apis/profiles_spec.rb
66
+ - mspec/assertions.rb
65
67
  - mspec/authentication_spec.rb
66
68
  - mspec/config_spec.rb
67
69
  - mspec/fixtures/profiles.xml
70
+ - mspec/helpers.rb
68
71
  - mspec/models/profile_spec.rb
69
72
  - mspec/spec_helper.rb
70
73
  - rgdata.gemspec