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 CHANGED
@@ -1,4 +1,5 @@
1
- == 1.0.0 / 2009-03-22
2
-
3
- * 1 major enhancement
4
- * Birthday!
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
@@ -1,28 +1,44 @@
1
1
  =grackle
2
2
  by Hayes Davis
3
- hayes@appozite.com
4
- http://www.appozite.com
5
- http://hayesdavis.net
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 is somewhat different
11
- from other Twitter API libraries. It does not try to hide the Twitter "methods" under an access layer nor does 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
- ==USING GRACKLE
19
+ Grackle supports both OAuth and HTTP basic authentication.
19
20
 
20
- ===Creating a Grackle::Client
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
- See Grackle::Client for more information about valid arguments to the constructor including for custom headers and SSL.
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
- - json_pure
101
- - httpclient
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.6"
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-03-23}
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.txt", "Rakefile", "grackle.gemspec", "lib/grackle.rb", "lib/grackle/client.rb", "lib/grackle/handlers.rb", "lib/grackle/transport.rb", "lib/grackle/utils.rb", "spec/grackle_spec.rb", "spec/spec_helper.rb", "test/test_grackle.rb"]
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.add_runtime_dependency(%q<httpclient>, [">= 0"])
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<httpclient>, [">= 0"])
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<httpclient>, [">= 0"])
37
+ s.add_dependency(%q<mime-types>, [">= 0"])
38
+ s.add_dependency(%q<oauth>, [">= 0"])
35
39
  end
36
40
  end
@@ -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
- APIS[api]
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
- APIS = {:rest=>'twitter.com',:search=>'search.twitter.com'}
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 :username, :password, :handlers, :default_format, :headers, :ssl, :api, :transport, :request
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 = {'User-Agent'=>'Grackle/1.0'}.merge!(options[: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,:username=>self.username,:password=>self.password,:headers=>headers,:params=>params
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)
@@ -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
- def post(string_url,options={})
21
- request(:post,url,options)
22
- end
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
- if file_param?(options[:params])
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 request_multipart(method, url, options={})
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
- add_form_data(req,options[:params])
73
- add_basic_auth(req,options[:username],options[:password])
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 add_basic_auth(req,username,password)
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.5'
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'
@@ -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,4 @@
1
+ Dir.glob("#{File.dirname(__FILE__)}/*.rb").each do |file|
2
+ require file
3
+ end
4
+
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require File.dirname(__FILE__) + '/../lib/grackle'
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.6
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-03-23 00:00:00 -07:00
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: httpclient
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.txt
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
@@ -1,7 +0,0 @@
1
-
2
- require File.join(File.dirname(__FILE__), %w[spec_helper])
3
-
4
- describe Grackle do
5
- end
6
-
7
- # EOF
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