hayesdavis-grackle 0.0.6 → 0.1.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.
- data/History.txt +5 -4
- data/{README.txt → README.rdoc} +30 -13
- data/grackle.gemspec +12 -8
- data/lib/grackle/client.rb +92 -15
- data/lib/grackle/transport.rb +79 -36
- data/lib/grackle.rb +5 -1
- data/test/test_client.rb +168 -0
- data/test/test_grackle.rb +4 -0
- data/test/test_handlers.rb +89 -0
- data/test/test_helper.rb +3 -0
- metadata +21 -10
- data/Rakefile +0 -35
- data/spec/grackle_spec.rb +0 -7
- data/spec/spec_helper.rb +0 -16
data/History.txt
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
== 1.0
|
2
|
-
|
3
|
-
*
|
4
|
-
|
1
|
+
== 0.1.0 (2009-04-12)
|
2
|
+
* Added OAuth authentication
|
3
|
+
* Deprecated :username and :password Grackle::Client constructor params
|
4
|
+
* Changed multipart upload implementation and removed dependency on httpclient gem
|
5
|
+
* Added dependency on mime-types gem
|
data/{README.txt → README.rdoc}
RENAMED
@@ -1,28 +1,44 @@
|
|
1
1
|
=grackle
|
2
2
|
by Hayes Davis
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
- hayes [at] appozite.com
|
4
|
+
- http://cheaptweet.com
|
5
|
+
- http://www.appozite.com
|
6
|
+
- http://hayesdavis.net
|
6
7
|
|
7
8
|
== DESCRIPTION
|
8
9
|
Grackle is a lightweight Ruby wrapper around the Twitter REST and Search APIs. It's based on my experience using the
|
9
10
|
Twitter API to build http://cheaptweet.com. The main goal of Grackle is to never require a release when the Twitter
|
10
|
-
API changes (which it often does) or in the face of a particular Twitter API bug. As such it
|
11
|
-
from other Twitter API libraries. It
|
11
|
+
API changes (which it often does) or in the face of a particular Twitter API bug. As such it's somewhat different
|
12
|
+
from other Twitter API libraries. It doesn't try to hide the Twitter "methods" under an access layer nor does it
|
12
13
|
introduce concrete classes for the various objects returned by Twitter. Instead, calls to the Grackle client map
|
13
14
|
directly to Twitter API URLs. The objects returned by API calls are generated as OpenStructs on the fly and make no
|
14
15
|
assumptions about the presence or absence of any particular attributes. Taking this approach means that changes to
|
15
16
|
URLs used by Twitter, parameters required by those URLs or return values will not require a new release. It
|
16
17
|
will potentially require, however, some modifications to your code that uses Grackle.
|
17
18
|
|
18
|
-
|
19
|
+
Grackle supports both OAuth and HTTP basic authentication.
|
19
20
|
|
20
|
-
|
21
|
+
==USING GRACKLE
|
21
22
|
|
23
|
+
Before you do anything else, you'll need to
|
22
24
|
require 'grackle'
|
23
|
-
client = Grackle::Client.new(:username=>'your_user',:password=>'yourpass')
|
24
25
|
|
25
|
-
|
26
|
+
===Creating a Grackle::Client
|
27
|
+
====Using Basic Auth
|
28
|
+
client = Grackle::Client.new(:auth=>{:type=:basic,:username=>'your_user',:password=>'yourpass'})
|
29
|
+
|
30
|
+
====Using OAuth
|
31
|
+
client = Grackle::Client.new(:auth=>{
|
32
|
+
:type=>:oauth,
|
33
|
+
:consumer_key=>'SOMECONSUMERKEYFROMTWITTER', :consumer_token=>'SOMECONSUMERTOKENFROMTWITTER',
|
34
|
+
:token=>'ACCESSTOKENACQUIREDONUSERSBEHALF', :token_secret=>'SUPERSECRETACCESSTOKENSECRET'
|
35
|
+
}}
|
36
|
+
|
37
|
+
====Using No Auth
|
38
|
+
client = Grackle::Client.new
|
39
|
+
|
40
|
+
See Grackle::Client for more information about valid arguments to the constructor. It's quite configurable. Among other things,
|
41
|
+
you can turn on ssl and specify custom headers. The calls below are pretty much as simple as it gets.
|
26
42
|
|
27
43
|
===Grackle Method Syntax
|
28
44
|
Grackle uses a method syntax that corresponds to the Twitter API URLs with a few twists. Where you would have a slash in
|
@@ -97,13 +113,14 @@ chain as described above, but use a "?" or "!" then the Grackle::Client.default_
|
|
97
113
|
== REQUIREMENTS:
|
98
114
|
|
99
115
|
You'll need the following gems to use all features of Grackle:
|
100
|
-
-
|
101
|
-
-
|
116
|
+
- json
|
117
|
+
- oauth
|
118
|
+
- mime-types
|
102
119
|
|
103
120
|
== INSTALL:
|
104
121
|
|
105
|
-
sudo gem sources -a http://gems.github.com
|
106
|
-
sudo gem install hayesdavis-grackle
|
122
|
+
sudo gem sources -a http://gems.github.com
|
123
|
+
sudo gem install hayesdavis-grackle
|
107
124
|
|
108
125
|
== LICENSE:
|
109
126
|
|
data/grackle.gemspec
CHANGED
@@ -2,21 +2,22 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{grackle}
|
5
|
-
s.version = "0.0
|
5
|
+
s.version = "0.1.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Hayes Davis"]
|
9
|
-
s.date = %q{2009-
|
9
|
+
s.date = %q{2009-04-12}
|
10
10
|
s.description = %q{Grackle is a lightweight library for the Twitter REST and Search API.}
|
11
11
|
s.email = %q{hayes@appozite.com}
|
12
|
-
s.files = ["History.txt", "README.
|
12
|
+
s.files = ["History.txt", "README.rdoc", "grackle.gemspec", "lib/grackle.rb", "lib/grackle/client.rb", "lib/grackle/handlers.rb", "lib/grackle/transport.rb", "lib/grackle/utils.rb", "test/test_grackle.rb", "test/test_helper.rb", "test/test_client.rb", "test/test_handlers.rb"]
|
13
13
|
s.has_rdoc = true
|
14
14
|
s.homepage = %q{http://github.com/hayesdavis/grackle}
|
15
|
-
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
15
|
+
s.rdoc_options = ["--inline-source", "--charset=UTF-8","--main=README.rdoc"]
|
16
|
+
s.extra_rdoc_files = ['README.rdoc']
|
16
17
|
s.require_paths = ["lib"]
|
17
18
|
s.rubyforge_project = %q{grackle}
|
18
19
|
s.rubygems_version = %q{1.3.1}
|
19
|
-
s.summary = %q{Grackle is a library for the Twitter REST and Search API designed to not require a new release in the face Twitter API changes or errors.}
|
20
|
+
s.summary = %q{Grackle is a library for the Twitter REST and Search API designed to not require a new release in the face Twitter API changes or errors. It supports both basic and OAuth authentication mechanisms.}
|
20
21
|
|
21
22
|
if s.respond_to? :specification_version then
|
22
23
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
@@ -24,13 +25,16 @@ Gem::Specification.new do |s|
|
|
24
25
|
|
25
26
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
27
|
s.add_runtime_dependency(%q<json>, [">= 0"])
|
27
|
-
s.
|
28
|
+
s.add_dependency(%q<mime-types>, [">= 0"])
|
29
|
+
s.add_dependency(%q<oauth>, [">= 0"])
|
28
30
|
else
|
29
31
|
s.add_dependency(%q<json>, [">= 0"])
|
30
|
-
s.add_dependency(%q<
|
32
|
+
s.add_dependency(%q<mime-types>, [">= 0"])
|
33
|
+
s.add_dependency(%q<oauth>, [">= 0"])
|
31
34
|
end
|
32
35
|
else
|
33
36
|
s.add_dependency(%q<json>, [">= 0"])
|
34
|
-
s.add_dependency(%q<
|
37
|
+
s.add_dependency(%q<mime-types>, [">= 0"])
|
38
|
+
s.add_dependency(%q<oauth>, [">= 0"])
|
35
39
|
end
|
36
40
|
end
|
data/lib/grackle/client.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module Grackle
|
2
2
|
|
3
|
+
#Returned by methods which retrieve data from the API
|
3
4
|
class TwitterStruct < OpenStruct
|
4
5
|
attr_accessor :id
|
5
6
|
end
|
6
7
|
|
8
|
+
#Raised by methods which call the API if a non-200 response status is received
|
7
9
|
class TwitterError < StandardError
|
8
10
|
attr_accessor :method, :request_uri, :status, :response_body, :response_object
|
9
11
|
|
@@ -18,12 +20,35 @@ module Grackle
|
|
18
20
|
|
19
21
|
# The Client is the public interface to Grackle. You build Twitter API calls using method chains. See the README for details
|
20
22
|
# and new for information on valid options.
|
23
|
+
#
|
24
|
+
# ==Authentication
|
25
|
+
# Twitter is migrating to OAuth as the preferred mechanism for authentication (over HTTP basic auth). Grackle supports both methods.
|
26
|
+
# Typically you will supply Grackle with authentication information at the time you create your Grackle::Client via the :auth parameter.
|
27
|
+
# ===Basic Auth
|
28
|
+
# client = Grackle.Client.new(:auth=>{:type=>:basic,:username=>'twitteruser',:password=>'secret'})
|
29
|
+
# Please note that the original way of specifying basic authentication still works but is deprecated
|
30
|
+
# client = Grackle.Client.new(:username=>'twitteruser',:password=>'secret') #deprecated
|
31
|
+
#
|
32
|
+
# ===OAuth
|
33
|
+
# OAuth is a relatively complex topic. For more information on OAuth applications see the official OAuth site at http://oauth.net and the
|
34
|
+
# OAuth specification at http://oauth.net/core/1.0. For authentication using OAuth, you will need do the following:
|
35
|
+
# - Acquire a key and token for your application ("Consumer" in OAuth terms) from Twitter. Learn more here: http://apiwiki.twitter.com/OAuth-FAQ
|
36
|
+
# - Acquire an access token and token secret for the user that will be using OAuth to authenticate into Twitter
|
37
|
+
# The process of acquiring the access token and token secret are outside the scope of Grackle and will need to be coded on a per-application
|
38
|
+
# basis. Grackle comes into play once you've acquired all of the above pieces of information. To create a Grackle::Client that uses OAuth once
|
39
|
+
# you've got all the necessary tokens and keys:
|
40
|
+
# client = Grackle::Client.new(:auth=>{
|
41
|
+
# :type=>:oauth,
|
42
|
+
# :consumer_key=>'SOMECONSUMERKEYFROMTWITTER, :consumer_token=>'SOMECONSUMERTOKENFROMTWITTER',
|
43
|
+
# :token=>'ACCESSTOKENACQUIREDONUSERSBEHALF', :token_secret=>'SUPERSECRETACCESSTOKENSECRET'
|
44
|
+
# }}
|
21
45
|
class Client
|
22
|
-
|
23
|
-
class Request
|
24
|
-
attr_accessor :path, :method, :api, :ssl
|
46
|
+
|
47
|
+
class Request #:nodoc:
|
48
|
+
attr_accessor :client, :path, :method, :api, :ssl
|
25
49
|
|
26
|
-
def initialize(api=:rest,ssl=true)
|
50
|
+
def initialize(client,api=:rest,ssl=true)
|
51
|
+
self.client = client
|
27
52
|
self.api = api
|
28
53
|
self.ssl = ssl
|
29
54
|
self.method = :get
|
@@ -43,7 +68,7 @@ module Grackle
|
|
43
68
|
end
|
44
69
|
|
45
70
|
def host
|
46
|
-
|
71
|
+
client.api_hosts[api]
|
47
72
|
end
|
48
73
|
|
49
74
|
def scheme
|
@@ -54,28 +79,50 @@ module Grackle
|
|
54
79
|
VALID_METHODS = [:get,:post,:put,:delete]
|
55
80
|
VALID_FORMATS = [:json,:xml,:atom,:rss]
|
56
81
|
|
57
|
-
|
82
|
+
TWITTER_API_HOSTS = {:rest=>'twitter.com',:search=>'search.twitter.com'}
|
83
|
+
|
84
|
+
#Basic OAuth information needed to communicate with Twitter
|
85
|
+
TWITTER_OAUTH_SPEC = {
|
86
|
+
:site=>'http://twitter.com',
|
87
|
+
:request_token_path=>'/oauth/request_token',
|
88
|
+
:access_token_path=>'/oauth/access_token',
|
89
|
+
:authorize_path=>'/oauth/authorize'
|
90
|
+
}
|
58
91
|
|
59
|
-
attr_accessor :
|
92
|
+
attr_accessor :auth, :handlers, :default_format, :headers, :ssl, :api, :transport, :request, :api_hosts
|
60
93
|
|
61
94
|
# Arguments (all are optional):
|
62
|
-
# - :username - twitter username to authenticate with
|
63
|
-
# - :password - twitter password to authenticate with
|
95
|
+
# - :username - twitter username to authenticate with (deprecated in favor of :auth arg)
|
96
|
+
# - :password - twitter password to authenticate with (deprecated in favor of :auth arg)
|
64
97
|
# - :handlers - Hash of formats to Handler instances (e.g. {:json=>CustomJSONHandler.new})
|
65
98
|
# - :default_format - Symbol of format to use when no format is specified in an API call (e.g. :json, :xml)
|
66
99
|
# - :headers - Hash of string keys and values for headers to pass in the HTTP request to twitter
|
67
100
|
# - :ssl - true or false to turn SSL on or off. Default is off (i.e. http://)
|
68
|
-
# - :api - one of :rest or :search
|
101
|
+
# - :api - one of :rest or :search. :rest is the default
|
102
|
+
# - :auth - Hash of authentication type and credentials. Must have :type key with value one of :basic or :oauth
|
103
|
+
# - :type=>:basic - Include :username and :password keys
|
104
|
+
# - :type=>:oauth - Include :consumer_key, :consumer_secret, :token and :token_secret keys
|
69
105
|
def initialize(options={})
|
70
106
|
self.transport = Transport.new
|
71
|
-
self.username = options.delete(:username)
|
72
|
-
self.password = options.delete(:password)
|
73
107
|
self.handlers = {:json=>Handlers::JSONHandler.new,:xml=>Handlers::XMLHandler.new,:unknown=>Handlers::StringHandler.new}
|
74
108
|
self.handlers.merge!(options[:handlers]||{})
|
75
109
|
self.default_format = options[:default_format] || :json
|
76
|
-
self.headers = {
|
110
|
+
self.headers = {"User-Agent"=>"Grackle/#{Grackle::VERSION}"}.merge!(options[:headers]||{})
|
77
111
|
self.ssl = options[:ssl] == true
|
78
112
|
self.api = options[:api] || :rest
|
113
|
+
self.api_hosts = TWITTER_API_HOSTS.clone
|
114
|
+
self.auth = {}
|
115
|
+
if options.has_key?(:username) || options.has_key?(:password)
|
116
|
+
#Use basic auth if :username and :password args are passed in
|
117
|
+
self.auth.merge!({:type=>:basic,:username=>options[:username],:password=>options[:password]})
|
118
|
+
end
|
119
|
+
#Handle auth mechanism that permits basic or oauth
|
120
|
+
if options.has_key?(:auth)
|
121
|
+
self.auth = options[:auth]
|
122
|
+
if auth[:type] == :oauth
|
123
|
+
self.auth = TWITTER_OAUTH_SPEC.merge(auth)
|
124
|
+
end
|
125
|
+
end
|
79
126
|
end
|
80
127
|
|
81
128
|
def method_missing(name,*args)
|
@@ -112,6 +159,36 @@ module Grackle
|
|
112
159
|
self.request = nil
|
113
160
|
end
|
114
161
|
|
162
|
+
#Deprecated in favor of using the auth attribute.
|
163
|
+
def username
|
164
|
+
if auth[:type] == :basic
|
165
|
+
auth[:username]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
#Deprecated in favor of using the auth attribute.
|
170
|
+
def username=(value)
|
171
|
+
unless auth[:type] == :basic
|
172
|
+
auth[:type] = :basic
|
173
|
+
end
|
174
|
+
auth[:username] = value
|
175
|
+
end
|
176
|
+
|
177
|
+
#Deprecated in favor of using the auth attribute.
|
178
|
+
def password
|
179
|
+
if auth[:type] == :basic
|
180
|
+
auth[:password]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
#Deprecated in favor of using the auth attribute.
|
185
|
+
def password=(value)
|
186
|
+
unless auth[:type] == :basic
|
187
|
+
auth[:type] = :basic
|
188
|
+
end
|
189
|
+
auth[:password] = value
|
190
|
+
end
|
191
|
+
|
115
192
|
protected
|
116
193
|
def call_with_format(format,params={})
|
117
194
|
id = params.delete(:id)
|
@@ -126,7 +203,7 @@ module Grackle
|
|
126
203
|
def send_request(params)
|
127
204
|
begin
|
128
205
|
transport.request(
|
129
|
-
request.method,request.url,:
|
206
|
+
request.method,request.url,:auth=>auth,:headers=>headers,:params=>params
|
130
207
|
)
|
131
208
|
rescue => e
|
132
209
|
puts e
|
@@ -150,7 +227,7 @@ module Grackle
|
|
150
227
|
end
|
151
228
|
|
152
229
|
def request
|
153
|
-
@request ||= Request.new(api,ssl)
|
230
|
+
@request ||= Request.new(self,api,ssl)
|
154
231
|
end
|
155
232
|
|
156
233
|
def handler(format)
|
data/lib/grackle/transport.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Grackle
|
2
2
|
|
3
|
-
class Response
|
3
|
+
class Response #:nodoc:
|
4
4
|
attr_accessor :method, :request_uri, :status, :body
|
5
5
|
|
6
6
|
def initialize(method,request_uri,status,body)
|
@@ -12,22 +12,10 @@ module Grackle
|
|
12
12
|
end
|
13
13
|
|
14
14
|
class Transport
|
15
|
-
|
16
|
-
def get(string_url,options={})
|
17
|
-
request(:get,url,options)
|
18
|
-
end
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def put(url,options={})
|
25
|
-
request(:put,url,options)
|
26
|
-
end
|
27
|
-
|
28
|
-
def delete(url,options={})
|
29
|
-
request(:delete,url,options)
|
30
|
-
end
|
16
|
+
attr_accessor :debug
|
17
|
+
|
18
|
+
CRLF = "\r\n"
|
31
19
|
|
32
20
|
def req_class(method)
|
33
21
|
case method
|
@@ -38,6 +26,11 @@ module Grackle
|
|
38
26
|
end
|
39
27
|
end
|
40
28
|
|
29
|
+
# Options are one of
|
30
|
+
# - :params - a hash of parameters to be sent with the request. If a File is a parameter value, \
|
31
|
+
# a multipart request will be sent. If a Time is included, .httpdate will be called on it.
|
32
|
+
# - :headers - a hash of headers to send with the request
|
33
|
+
# - :auth - a hash of authentication parameters for either basic or oauth
|
41
34
|
def request(method, string_url, options={})
|
42
35
|
params = stringify_params(options[:params])
|
43
36
|
if method == :get && params
|
@@ -45,35 +38,33 @@ module Grackle
|
|
45
38
|
end
|
46
39
|
url = URI.parse(string_url)
|
47
40
|
begin
|
48
|
-
|
49
|
-
request_multipart(method,url,options)
|
50
|
-
else
|
51
|
-
request_standard(method,url,options)
|
52
|
-
end
|
41
|
+
execute_request(method,url,options)
|
53
42
|
rescue Timeout::Error
|
54
43
|
raise "Timeout while #{method}ing #{url.to_s}"
|
55
44
|
end
|
56
45
|
end
|
57
46
|
|
58
|
-
def
|
59
|
-
require 'httpclient' unless defined? HTTPClient
|
60
|
-
client = HTTPClient.new
|
61
|
-
if options[:username] && options[:password]
|
62
|
-
client.set_auth(url.to_s,options.delete(:username),options.delete(:password))
|
63
|
-
end
|
64
|
-
res = client.request(method,url.to_s,nil,options[:params],options[:headers])
|
65
|
-
Response.new(method,url.to_s,res.status,res.content)
|
66
|
-
end
|
67
|
-
|
68
|
-
def request_standard(method,url,options={})
|
47
|
+
def execute_request(method,url,options={})
|
69
48
|
Net::HTTP.new(url.host, url.port).start do |http|
|
70
49
|
req = req_class(method).new(url.request_uri)
|
71
50
|
add_headers(req,options[:headers])
|
72
|
-
|
73
|
-
|
51
|
+
if file_param?(options[:params])
|
52
|
+
add_multipart_data(req,options[:params])
|
53
|
+
else
|
54
|
+
add_form_data(req,options[:params])
|
55
|
+
end
|
56
|
+
if options.has_key? :auth
|
57
|
+
if options[:auth][:type] == :basic
|
58
|
+
add_basic_auth(req,options[:auth])
|
59
|
+
elsif options[:auth][:type] == :oauth
|
60
|
+
add_oauth(req,options[:auth])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
dump_request(req) if debug
|
74
64
|
res = http.request(req)
|
65
|
+
dump_response(res) if debug
|
75
66
|
Response.new(method,url.to_s,res.code.to_i,res.body)
|
76
|
-
end
|
67
|
+
end
|
77
68
|
end
|
78
69
|
|
79
70
|
def query_string(params)
|
@@ -128,10 +119,62 @@ module Grackle
|
|
128
119
|
end
|
129
120
|
end
|
130
121
|
|
131
|
-
def
|
122
|
+
def add_multipart_data(req,params)
|
123
|
+
boundary = Time.now.to_i.to_s(16)
|
124
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
125
|
+
body = ""
|
126
|
+
params.each do |key,value|
|
127
|
+
esc_key = url_encode(key)
|
128
|
+
body << "--#{boundary}#{CRLF}"
|
129
|
+
if value.respond_to?(:read)
|
130
|
+
mime_type = MIME::Types.type_for(value.path)[0] || MIME::Types["application/octet-stream"][0]
|
131
|
+
body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
|
132
|
+
body << "Content-Type: #{mime_type.simplified}#{CRLF*2}"
|
133
|
+
body << value.read
|
134
|
+
else
|
135
|
+
body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
|
136
|
+
end
|
137
|
+
body << CRLF
|
138
|
+
end
|
139
|
+
body << "--#{boundary}--#{CRLF*2}"
|
140
|
+
req.body = body
|
141
|
+
req["Content-Length"] = req.body.size
|
142
|
+
end
|
143
|
+
|
144
|
+
def add_basic_auth(req,auth)
|
145
|
+
username = auth[:username]
|
146
|
+
password = auth[:password]
|
132
147
|
if username && password
|
133
148
|
req.basic_auth(username,password)
|
134
149
|
end
|
135
150
|
end
|
151
|
+
|
152
|
+
def add_oauth(req,auth)
|
153
|
+
options = auth.reject do |key,value|
|
154
|
+
[:type,:consumer_key,:consumer_secret,:token,:token_secret].include?(key)
|
155
|
+
end
|
156
|
+
consumer = OAuth::Consumer.new(auth[:consumer_key],auth[:consumer_secret],options)
|
157
|
+
access_token = OAuth::AccessToken.new(consumer,auth[:token],auth[:token_secret])
|
158
|
+
consumer.sign!(req,access_token)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
def dump_request(req)
|
163
|
+
puts "Sending Request"
|
164
|
+
puts"#{req.method} #{req.path}"
|
165
|
+
dump_headers(req)
|
166
|
+
end
|
167
|
+
|
168
|
+
def dump_response(res)
|
169
|
+
puts "Received Response"
|
170
|
+
dump_headers(res)
|
171
|
+
puts res.body
|
172
|
+
end
|
173
|
+
|
174
|
+
def dump_headers(msg)
|
175
|
+
msg.each_header do |key, value|
|
176
|
+
puts "\t#{key}=#{value}"
|
177
|
+
end
|
178
|
+
end
|
136
179
|
end
|
137
180
|
end
|
data/lib/grackle.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Grackle
|
2
2
|
|
3
3
|
# :stopdoc:
|
4
|
-
VERSION = '0.0
|
4
|
+
VERSION = '0.1.0'
|
5
5
|
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
6
6
|
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
7
7
|
# :startdoc:
|
@@ -18,8 +18,12 @@ $:.unshift File.dirname(__FILE__)
|
|
18
18
|
require 'ostruct'
|
19
19
|
require 'open-uri'
|
20
20
|
require 'net/http'
|
21
|
+
require 'time'
|
21
22
|
require 'rexml/document'
|
22
23
|
require 'json'
|
24
|
+
require 'oauth'
|
25
|
+
require 'oauth/client'
|
26
|
+
require 'mime/types'
|
23
27
|
|
24
28
|
require 'grackle/utils'
|
25
29
|
require 'grackle/transport'
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class TestClient < Test::Unit::TestCase
|
4
|
+
|
5
|
+
#Used for mocking HTTP requests
|
6
|
+
class Net::HTTP
|
7
|
+
class << self
|
8
|
+
attr_accessor :response, :request
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(req)
|
12
|
+
self.class.request = req
|
13
|
+
self.class.response
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
#Mock responses that conform mostly to HTTPResponse's interface
|
18
|
+
class MockResponse
|
19
|
+
include Net::HTTPHeader
|
20
|
+
attr_accessor :code, :body
|
21
|
+
def initialize(code,body,headers={})
|
22
|
+
self.code = code
|
23
|
+
self.body = body
|
24
|
+
headers.each do |name, value|
|
25
|
+
self[name] = value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#Transport that collects info on requests and responses for testing purposes
|
31
|
+
class MockTransport < Grackle::Transport
|
32
|
+
attr_accessor :status, :body, :method, :url, :options
|
33
|
+
|
34
|
+
def initialize(status,body,headers={})
|
35
|
+
Net::HTTP.response = MockResponse.new(status,body,headers)
|
36
|
+
end
|
37
|
+
|
38
|
+
def request(method, string_url, options)
|
39
|
+
self.method = method
|
40
|
+
self.url = URI.parse(string_url)
|
41
|
+
self.options = options
|
42
|
+
super(method,string_url,options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class TestHandler
|
47
|
+
attr_accessor :decode_value
|
48
|
+
|
49
|
+
def initialize(value)
|
50
|
+
self.decode_value = value
|
51
|
+
end
|
52
|
+
|
53
|
+
def decode_response(body)
|
54
|
+
decode_value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_simple_get_request
|
59
|
+
client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
|
60
|
+
value = client.users.show.json? :screen_name=>'test_user'
|
61
|
+
assert_equal(:get,client.transport.method)
|
62
|
+
assert_equal('http',client.transport.url.scheme)
|
63
|
+
assert_equal('twitter.com',client.transport.url.host)
|
64
|
+
assert_equal('/users/show.json',client.transport.url.path)
|
65
|
+
assert_equal('test_user',client.transport.options[:params][:screen_name])
|
66
|
+
assert_equal('screen_name=test_user',Net::HTTP.request.path.split(/\?/)[1])
|
67
|
+
assert_equal(12345,value.id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_simple_post_request_with_basic_auth
|
71
|
+
client = Grackle::Client.new(:auth=>{:type=>:basic,:username=>'fake_user',:password=>'fake_pass'})
|
72
|
+
test_simple_post(client) do
|
73
|
+
assert_match(/Basic/i,Net::HTTP.request['Authorization'],"Request should include Authorization header for basic auth")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_simple_post_request_with_oauth
|
78
|
+
client = Grackle::Client.new(:auth=>{:type=>:oauth,:consumer_key=>'12345',:consumer_secret=>'abc',:token=>'wxyz',:token_secret=>'98765'})
|
79
|
+
test_simple_post(client) do
|
80
|
+
auth = Net::HTTP.request['Authorization']
|
81
|
+
assert_match(/OAuth/i,auth,"Request should include Authorization header for OAuth")
|
82
|
+
assert_match(/oauth_consumer_key="12345"/,auth,"Auth header should include consumer key")
|
83
|
+
assert_match(/oauth_token="wxyz"/,auth,"Auth header should include token")
|
84
|
+
assert_match(/oauth_signature_method="HMAC-SHA1"/,auth,"Auth header should include HMAC-SHA1 signature method as that's what Twitter supports")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_ssl
|
89
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]',:ssl=>true)
|
90
|
+
client.statuses.public_timeline?
|
91
|
+
assert_equal("https",client.transport.url.scheme)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_default_format
|
95
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]',:default_format=>:json)
|
96
|
+
client.statuses.public_timeline?
|
97
|
+
assert_match(/\.json$/,client.transport.url.path)
|
98
|
+
|
99
|
+
client = new_client(200,'<statuses type="array"><status><id>1</id><text>test 1</text></status></statuses>',:default_format=>:xml)
|
100
|
+
client.statuses.public_timeline?
|
101
|
+
assert_match(/\.xml$/,client.transport.url.path)
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_api
|
105
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]',:api=>:search)
|
106
|
+
client.search? :q=>'test'
|
107
|
+
assert_equal('search.twitter.com',client.transport.url.host)
|
108
|
+
client[:rest].users.show.some_user?
|
109
|
+
assert_equal('twitter.com',client.transport.url.host)
|
110
|
+
client.api = :search
|
111
|
+
client.trends?
|
112
|
+
assert_equal('search.twitter.com',client.transport.url.host)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_headers
|
116
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]',:headers=>{'User-Agent'=>'TestAgent/1.0','X-Test-Header'=>'Header Value'})
|
117
|
+
client.statuses.public_timeline?
|
118
|
+
assert_equal('TestAgent/1.0',Net::HTTP.request['User-Agent'],"Custom User-Agent header should have been set")
|
119
|
+
assert_equal('Header Value',Net::HTTP.request['X-Test-Header'],"Custom X-Test-Header header should have been set")
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_custom_handlers
|
123
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]',:handlers=>{:json=>TestHandler.new(42)})
|
124
|
+
value = client.statuses.public_timeline.json?
|
125
|
+
assert_equal(42,value)
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_clear
|
129
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]')
|
130
|
+
client.some.url.that.does.not.exist
|
131
|
+
assert_equal('/some/url/that/does/not/exist',client.send(:request).path,"An unexecuted path should be build up")
|
132
|
+
client.clear
|
133
|
+
assert_equal('',client.send(:request).path,"The path shoudl be cleared")
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_file_param_triggers_multipart_encoding
|
137
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]')
|
138
|
+
client.account.update_profile_image! :image=>File.new(__FILE__)
|
139
|
+
assert_match(/multipart\/form-data/,Net::HTTP.request['Content-Type'])
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_time_param_is_http_encoded_and_escaped
|
143
|
+
client = new_client(200,'[{"id":1,"text":"test 1"}]')
|
144
|
+
time = Time.now-60*60
|
145
|
+
client.statuses.public_timeline? :since=>time
|
146
|
+
assert_equal("/statuses/public_timeline.json?since=#{CGI::escape(time.httpdate)}",Net::HTTP.request.path)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def new_client(response_status, response_body, client_opts={})
|
151
|
+
client = Grackle::Client.new(client_opts)
|
152
|
+
client.transport = MockTransport.new(response_status,response_body)
|
153
|
+
client
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_simple_post(client)
|
157
|
+
client.transport = MockTransport.new(200,'{"id":12345,"text":"test status"}')
|
158
|
+
value = client.statuses.update! :status=>'test status'
|
159
|
+
assert_equal(:post,client.transport.method,"Expected post request")
|
160
|
+
assert_equal('http',client.transport.url.scheme,"Expected scheme to be http")
|
161
|
+
assert_equal('twitter.com',client.transport.url.host,"Expected request to be against twitter.com")
|
162
|
+
assert_equal('/statuses/update.json',client.transport.url.path)
|
163
|
+
assert_match(/status=test%20status/,Net::HTTP.request.body,"Parameters should be form encoded")
|
164
|
+
assert_equal(12345,value.id)
|
165
|
+
yield(client) if block_given?
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
data/test/test_grackle.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class TestHandlers < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_string_handler_echoes
|
6
|
+
sh = Grackle::Handlers::StringHandler.new
|
7
|
+
body = "This is some text"
|
8
|
+
assert_equal(body,sh.decode_response(body),"String handler should just echo response body")
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_xml_handler_parses_text_only_nodes_as_attributes
|
12
|
+
h = Grackle::Handlers::XMLHandler.new
|
13
|
+
body = "<user><id>12345</id><screen_name>User1</screen_name></user>"
|
14
|
+
value = h.decode_response(body)
|
15
|
+
assert_equal(12345,value.id,"Id element should be treated as an attribute and be returned as a Fixnum")
|
16
|
+
assert_equal("User1",value.screen_name,"screen_name element should be treated as an attribute")
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_xml_handler_parses_nested_elements_with_children_as_nested_objects
|
20
|
+
h = Grackle::Handlers::XMLHandler.new
|
21
|
+
body = "<user><id>12345</id><screen_name>User1</screen_name><status><id>9876</id><text>this is a status</text></status></user>"
|
22
|
+
value = h.decode_response(body)
|
23
|
+
assert_not_nil(value.status,"status element should be turned into an object")
|
24
|
+
assert_equal(9876,value.status.id,"status element should have id")
|
25
|
+
assert_equal("this is a status",value.status.text,"status element should have text")
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_xml_handler_parses_elements_with_type_array_as_arrays
|
29
|
+
h = Grackle::Handlers::XMLHandler.new
|
30
|
+
body = "<some_ids type=\"array\">"
|
31
|
+
1.upto(10) do |i|
|
32
|
+
body << "<id>#{i}</id>"
|
33
|
+
end
|
34
|
+
body << "</some_ids>"
|
35
|
+
value = h.decode_response(body)
|
36
|
+
assert_equal(Array,value.class,"Root parsed object should be an array")
|
37
|
+
assert_equal(10,value.length,"Parsed array should have correct length")
|
38
|
+
0.upto(9) do |i|
|
39
|
+
assert_equal(i+1,value[i],"Parsed array should contain #{i+1} at index #{i}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_xml_handler_parses_certain_elements_as_arrays
|
44
|
+
h = Grackle::Handlers::XMLHandler.new
|
45
|
+
special_twitter_elements = ['ids','statuses','users']
|
46
|
+
special_twitter_elements.each do |name|
|
47
|
+
body = "<#{name}>"
|
48
|
+
1.upto(10) do |i|
|
49
|
+
body << "<complex_value><id>#{i}</id><profile>This is profile #{i}</profile></complex_value>"
|
50
|
+
end
|
51
|
+
body << "</#{name}>"
|
52
|
+
value = h.decode_response(body)
|
53
|
+
assert_equal(Array,value.class,"Root parsed object should be an array")
|
54
|
+
assert_equal(10,value.length,"Parsed array should have correct length")
|
55
|
+
0.upto(9) do |i|
|
56
|
+
assert_equal(i+1,value[i].id,"Parsed array should contain id #{i+1} at index #{i}")
|
57
|
+
assert_equal("This is profile #{i+1}",value[i].profile,"Parsed array should contain profile 'This is profile #{i+1}' at index #{i}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_json_handler_parses_basic_attributes
|
63
|
+
h = Grackle::Handlers::JSONHandler.new
|
64
|
+
body = '{"id":12345,"screen_name":"User1"}'
|
65
|
+
value = h.decode_response(body)
|
66
|
+
assert_equal(12345,value.id,"Id element should be treated as an attribute and be returned as a Fixnum")
|
67
|
+
assert_equal("User1",value.screen_name,"screen_name element should be treated as an attribute")
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_json_handler_parses_complex_attributes
|
71
|
+
h = Grackle::Handlers::JSONHandler.new
|
72
|
+
body = '{"id":12345,"screen_name":"User1","statuses":['
|
73
|
+
1.upto(10) do |i|
|
74
|
+
user_id = i+5000
|
75
|
+
body << ',' unless i == 1
|
76
|
+
body << %Q{{"id":#{i},"text":"Status from user #{user_id}", "user":{"id":#{user_id},"screen_name":"User #{user_id}"}}}
|
77
|
+
end
|
78
|
+
body << ']}'
|
79
|
+
value = h.decode_response(body)
|
80
|
+
assert_equal(12345,value.id,"Id element should be treated as an attribute and be returned as a Fixnum")
|
81
|
+
assert_equal("User1",value.screen_name,"screen_name element should be treated as an attribute")
|
82
|
+
assert_equal(Array,value.statuses.class,"statuses attribute should be an array")
|
83
|
+
1.upto(10) do |i|
|
84
|
+
assert_equal(i,value.statuses[i-1].id,"array should contain status with id #{i} at index #{i-1}")
|
85
|
+
assert_equal(i+5000,value.statuses[i-1].user.id,"status at index #{i-1} should contain user with id #{i+5000}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hayesdavis-grackle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hayes Davis
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-04-12 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -23,7 +23,17 @@ dependencies:
|
|
23
23
|
version: "0"
|
24
24
|
version:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
26
|
+
name: mime-types
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: oauth
|
27
37
|
type: :runtime
|
28
38
|
version_requirement:
|
29
39
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -38,27 +48,28 @@ executables: []
|
|
38
48
|
|
39
49
|
extensions: []
|
40
50
|
|
41
|
-
extra_rdoc_files:
|
42
|
-
|
51
|
+
extra_rdoc_files:
|
52
|
+
- README.rdoc
|
43
53
|
files:
|
44
54
|
- History.txt
|
45
|
-
- README.
|
46
|
-
- Rakefile
|
55
|
+
- README.rdoc
|
47
56
|
- grackle.gemspec
|
48
57
|
- lib/grackle.rb
|
49
58
|
- lib/grackle/client.rb
|
50
59
|
- lib/grackle/handlers.rb
|
51
60
|
- lib/grackle/transport.rb
|
52
61
|
- lib/grackle/utils.rb
|
53
|
-
- spec/grackle_spec.rb
|
54
|
-
- spec/spec_helper.rb
|
55
62
|
- test/test_grackle.rb
|
63
|
+
- test/test_helper.rb
|
64
|
+
- test/test_client.rb
|
65
|
+
- test/test_handlers.rb
|
56
66
|
has_rdoc: true
|
57
67
|
homepage: http://github.com/hayesdavis/grackle
|
58
68
|
post_install_message:
|
59
69
|
rdoc_options:
|
60
70
|
- --inline-source
|
61
71
|
- --charset=UTF-8
|
72
|
+
- --main=README.rdoc
|
62
73
|
require_paths:
|
63
74
|
- lib
|
64
75
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -79,6 +90,6 @@ rubyforge_project: grackle
|
|
79
90
|
rubygems_version: 1.2.0
|
80
91
|
signing_key:
|
81
92
|
specification_version: 2
|
82
|
-
summary: Grackle is a library for the Twitter REST and Search API designed to not require a new release in the face Twitter API changes or errors.
|
93
|
+
summary: Grackle is a library for the Twitter REST and Search API designed to not require a new release in the face Twitter API changes or errors. It supports both basic and OAuth authentication mechanisms.
|
83
94
|
test_files: []
|
84
95
|
|
data/Rakefile
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
-
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
-
# are where the options are used.
|
4
|
-
|
5
|
-
begin
|
6
|
-
require 'bones'
|
7
|
-
Bones.setup
|
8
|
-
rescue LoadError
|
9
|
-
begin
|
10
|
-
load 'tasks/setup.rb'
|
11
|
-
rescue LoadError
|
12
|
-
raise RuntimeError, '### please install the "bones" gem ###'
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
ensure_in_path 'lib'
|
17
|
-
require 'grackle'
|
18
|
-
|
19
|
-
task :default => 'spec:run'
|
20
|
-
|
21
|
-
PROJ.name = 'grackle'
|
22
|
-
PROJ.authors = 'Hayes Davis'
|
23
|
-
PROJ.email = 'hayes@appozite.com'
|
24
|
-
PROJ.url = 'http://github.com/hayesdavis/grackle'
|
25
|
-
PROJ.version = Grackle::VERSION
|
26
|
-
PROJ.rubyforge.name = 'grackle'
|
27
|
-
PROJ.summary = 'Grackle is a library for the Twitter REST and Search API'
|
28
|
-
PROJ.description = 'Grackle is a library for the Twitter REST and Search API that aims to go with the flow.'
|
29
|
-
PROJ.spec.opts << '--color'
|
30
|
-
PROJ.exclude = %w(.git pkg)
|
31
|
-
|
32
|
-
depend_on 'json'
|
33
|
-
depend_on 'httpclient'
|
34
|
-
|
35
|
-
# EOF
|
data/spec/grackle_spec.rb
DELETED
data/spec/spec_helper.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
|
2
|
-
require File.expand_path(
|
3
|
-
File.join(File.dirname(__FILE__), %w[.. lib grackle]))
|
4
|
-
|
5
|
-
Spec::Runner.configure do |config|
|
6
|
-
# == Mock Framework
|
7
|
-
#
|
8
|
-
# RSpec uses it's own mocking framework by default. If you prefer to
|
9
|
-
# use mocha, flexmock or RR, uncomment the appropriate line:
|
10
|
-
#
|
11
|
-
# config.mock_with :mocha
|
12
|
-
# config.mock_with :flexmock
|
13
|
-
# config.mock_with :rr
|
14
|
-
end
|
15
|
-
|
16
|
-
# EOF
|