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