rgdata 0.1.0 → 0.2.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.
- data/README.rdoc +39 -13
- data/lib/rgdata/api.rb +17 -5
- data/lib/rgdata/authentication.rb +21 -10
- data/lib/rgdata/version.rb +1 -1
- data/lib/support/hash.rb +2 -2
- data/lib/support/object.rb +7 -0
- data/mspec/apis/profiles_spec.rb +2 -4
- data/mspec/assertions.rb +15 -0
- data/mspec/authentication_spec.rb +15 -3
- data/mspec/helpers.rb +9 -0
- data/mspec/spec_helper.rb +13 -10
- metadata +5 -2
data/README.rdoc
CHANGED
@@ -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
|
-
|
11
|
-
#
|
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
|
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
|
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(
|
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
|
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
|
-
*
|
42
|
+
* expires_in: You have this many seconds to use access_token before it expires.
|
42
43
|
* refresh_token: See below.
|
43
|
-
*
|
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.
|
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.
|
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
|
-
===
|
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.
|
data/lib/rgdata/api.rb
CHANGED
@@ -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 = {
|
26
|
-
|
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
|
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
|
-
|
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.
|
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
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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.
|
31
|
-
|
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
|
data/lib/rgdata/version.rb
CHANGED
data/lib/support/hash.rb
CHANGED
@@ -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
|
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).
|
11
|
+
if extract(key).blank?
|
12
12
|
raise "Option '#{key}' cannot be nil"
|
13
13
|
end
|
14
14
|
end
|
data/mspec/apis/profiles_spec.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
|
data/mspec/assertions.rb
ADDED
@@ -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
|
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 '
|
14
|
-
RGData::Authentication.
|
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
|
data/mspec/helpers.rb
ADDED
data/mspec/spec_helper.rb
CHANGED
@@ -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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
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-
|
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
|