grackle 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,25 @@
1
+ == 0.1.5 (2009-10-28)
2
+ Added support for the Twitter version 1 API as a API symbol :v1
3
+
4
+ == 0.1.4 (2009-08-09)
5
+ Added additional check for 3xx responses that don't have location headers.
6
+
7
+ == 0.1.3 (2009-08-09)
8
+ Merged in changes from gotwalt for timeouts and redirects. Revised 30x
9
+ redirect handling to support a limit that prevents infinite redirects.
10
+
11
+ == 0.1.2 (2009-05-11)
12
+ Changed :site param used by OAuth to be determined dynamically unless
13
+ explicitly specified as part of the :auth param to the Client constructor.
14
+ This param needs to match the scheme and authority of the request using
15
+ OAuth or the signing will not validate.
16
+
17
+ == 0.1.1 (2009-05-10)
18
+ Fixed issue where SSL setting wasn't being applied correctly to Net:HTTP
19
+ which was preventing SSL-enabled requests from working correctly.
20
+
21
+ == 0.1.0 (2009-04-12)
22
+ * Added OAuth authentication
23
+ * Deprecated :username and :password Grackle::Client constructor params
24
+ * Changed multipart upload implementation and removed dependency on httpclient gem
25
+ * Added dependency on mime-types gem
data/README.rdoc ADDED
@@ -0,0 +1,171 @@
1
+ =grackle
2
+ by Hayes Davis
3
+ - http://twitter.com/hayesdavis
4
+ - hayes [at] appozite.com
5
+ - http://cheaptweet.com
6
+ - http://www.appozite.com
7
+ - http://hayesdavis.net
8
+
9
+ == DESCRIPTION
10
+ Grackle is a lightweight Ruby wrapper around the Twitter REST and Search APIs. It's based on my experience using the
11
+ Twitter API to build http://cheaptweet.com. The main goal of Grackle is to never require a release when the Twitter
12
+ API changes (which it often does) or in the face of a particular Twitter API bug. As such it's somewhat different
13
+ from other Twitter API libraries. It doesn't try to hide the Twitter "methods" under an access layer nor does it
14
+ introduce concrete classes for the various objects returned by Twitter. Instead, calls to the Grackle client map
15
+ directly to Twitter API URLs. The objects returned by API calls are generated as OpenStructs on the fly and make no
16
+ assumptions about the presence or absence of any particular attributes. Taking this approach means that changes to
17
+ URLs used by Twitter, parameters required by those URLs or return values will not require a new release. It
18
+ will potentially require, however, some modifications to your code that uses Grackle.
19
+
20
+ Grackle supports both OAuth and HTTP basic authentication.
21
+
22
+ ==USING GRACKLE
23
+
24
+ Before you do anything else, you'll need to
25
+ require 'grackle'
26
+
27
+ ===Creating a Grackle::Client
28
+ ====Using Basic Auth
29
+ client = Grackle::Client.new(:auth=>{:type=>:basic,:username=>'your_user',:password=>'yourpass'})
30
+
31
+ ====Using OAuth
32
+ client = Grackle::Client.new(:auth=>{
33
+ :type=>:oauth,
34
+ :consumer_key=>'SOMECONSUMERKEYFROMTWITTER', :consumer_secret=>'SOMECONSUMERTOKENFROMTWITTER',
35
+ :token=>'ACCESSTOKENACQUIREDONUSERSBEHALF', :token_secret=>'SUPERSECRETACCESSTOKENSECRET'
36
+ })
37
+
38
+ ====Using No Auth
39
+ client = Grackle::Client.new
40
+
41
+ See Grackle::Client for more information about valid arguments to the constructor. It's quite configurable. Among other things,
42
+ you can turn on ssl and specify custom headers. The calls below are pretty much as simple as it gets.
43
+
44
+ ===Grackle Method Syntax
45
+ Grackle uses a method syntax that corresponds to the Twitter API URLs with a few twists. Where you would have a slash in
46
+ a Twitter URL, that becomes a "." in a chained set of Grackle method calls. Each call in the method chain is used to build
47
+ Twitter URL path until a particular call is encountered which causes the request to be sent. Methods which will cause a
48
+ request to be execute include:
49
+ - A method call ending in "?" will cause an HTTP GET to be executed
50
+ - A method call ending in "!" will cause an HTTP POST to be executed
51
+ - If a valid format such as .json, .xml, .rss or .atom is encounted, a get will be executed with that format
52
+ - A format method can also include a ? or ! to determine GET or POST in that format respectively
53
+
54
+ ===GETting Data
55
+ The preferred and simplest way of executing a GET is to use the "?" method notation. This will use the default client
56
+ format (usually JSON, but see Formats section below):
57
+ client.users.show? :screen_name=>'some_user' #http://twitter.com/users/show.json?screen_name=some_user
58
+
59
+ You can force XML format by doing:
60
+ client.users.show.xml? :screen_name=>'some_user' #http://twitter.com/users/show.xml?scren_name=some_user
61
+
62
+ You can force JSON:
63
+ client.users.show.json? :screen_name=>'some_user' #http://twitter.com/users/show.json?screen_name=some_user
64
+
65
+ Or, since Twitter also allows certain ids/screen_names to be part of their URLs, this works:
66
+ client.users.show.some_user? #http://twitter.com/users/show/some_user.json
67
+
68
+ If you use an explicit format, you can leave off the "?" like so:
69
+ client.users.show.xml :screen_name=>'some_user' #http://twitter.com/users/show.xml?scren_name=some_user
70
+
71
+ ===POSTing data
72
+ To use Twitter API methods that require an HTTP POST, you need to end your method chain with a bang (!)
73
+
74
+ The preferred way is to use the Client's default format (usually JSON, but see Formats section below):
75
+ client.statuses.update! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.json
76
+
77
+ You can force a format. To update the authenticated user's status using the XML format:
78
+ client.statuses.update.xml! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.xml
79
+
80
+ Or, with JSON
81
+ client.statuses.update.json! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.json
82
+
83
+ ===Toggling APIs
84
+ By default, the Grackle::Client sends all requests to the unversioned Twitter REST API. If you want to send requests to
85
+ the Twitter Search API, just set Grackle::Client.api to :search. To toggle back, set it to be :rest. All requests made
86
+ after setting this attribute will go to that API.
87
+
88
+ If you want to make a specific request to one API and not change the Client's overall api setting beyond that request, you can use the
89
+ bracket syntax like so:
90
+ client[:search].trends.daily? :exclude=>'hashtags'
91
+ client[:rest].users.show? :id=>'hayesdavis'
92
+
93
+ Search and REST requests are all built using the same method chaining and termination conventions.
94
+
95
+ Twitter is introducing API versioning. Grackle now supports the version 1 API using the :v1 API name. When using the :v1
96
+ API, resources that were previously separated between the search and REST APIs are available under one unified API. To
97
+ use the :v1 API, do the following:
98
+ client[:v1].search? :q=>'grackle'
99
+ client[:v1].users.show? :id=>'hayesdavis'
100
+
101
+ If you want to use the :v1 API for all requests, you may set Grackle::Client.api to :v1 or specify the :api option in the
102
+ Grackle::Client constructor like:
103
+ client = Grackle::Client.new(:api=>:v1)
104
+
105
+ ===Parameter handling
106
+ - All parameters are URL encoded as necessary.
107
+ - If you use a File object as a parameter it will be POSTed to Twitter in a multipart request.
108
+ - If you use a Time object as a parameter, .httpdate will be called on it and that value will be used
109
+
110
+ ===Return Values
111
+ Regardless of the format used, Grackle returns an OpenStruct (actually a Grackle::TwitterStruct) of data. The attributes
112
+ available on these structs correspond to the data returned by Twitter.
113
+
114
+ ===Dealing with Errors
115
+ If the request to Twitter does not return a status code of 200, then a TwitterError is thrown. This contains the HTTP method used,
116
+ the full request URI, the response status, the response body in text and a response object build by parsing the formatted error
117
+ returned by Twitter. It's a good idea to wrap your API calls with rescue clauses for Grackle::TwitterError.
118
+
119
+ If there is an unexpected connection error or Twitter returns data in the wrong format (which it can do), you'll still get a TwitterError.
120
+
121
+ ===Formats
122
+ Twitter allows you to request data in particular formats. Grackle automatically parses JSON and XML formatted responses
123
+ and returns an OpenStruct. If you specify a format that Grackle doesn't parse for you, you'll receive a string containing
124
+ the raw response body. The Grackle::Client has a default_format you can specify. By default, the default_format is :json.
125
+ If you don't include a named format in your method chain as described above, but use a "?" or "!" then the
126
+ Grackle::Client.default_format is used.
127
+
128
+ == REQUIREMENTS:
129
+
130
+ You'll need the following gems to use all features of Grackle:
131
+ - json
132
+ - oauth
133
+ - mime-types
134
+
135
+ == INSTALL:
136
+ The grackle gem is now hosted at http://gemcutter.org. If you've already setup gemcutter
137
+ in your sources, you can do the following:
138
+ sudo gem install grackle
139
+
140
+ If you haven't yet setup gemcutter in your sources, go to http://gemcutter.org and follow the instructions there.
141
+ They will likely tell you to do the following:
142
+ sudo gem install gemcutter
143
+ sudo gem tumble
144
+
145
+ Once you've done that you can do:
146
+ sudo gem install grackle
147
+
148
+ == LICENSE:
149
+
150
+ (The MIT License)
151
+
152
+ Copyright (c) 2009
153
+
154
+ Permission is hereby granted, free of charge, to any person obtaining
155
+ a copy of this software and associated documentation files (the
156
+ 'Software'), to deal in the Software without restriction, including
157
+ without limitation the rights to use, copy, modify, merge, publish,
158
+ distribute, sublicense, and/or sell copies of the Software, and to
159
+ permit persons to whom the Software is furnished to do so, subject to
160
+ the following conditions:
161
+
162
+ The above copyright notice and this permission notice shall be
163
+ included in all copies or substantial portions of the Software.
164
+
165
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
166
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
167
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
168
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
169
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
170
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
171
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/grackle.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{grackle}
5
+ s.version = "0.1.5"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Hayes Davis"]
9
+ s.date = %q{2009-05-23}
10
+ s.description = %q{Grackle is a lightweight library for the Twitter REST and Search API.}
11
+ s.email = %q{hayes@appozite.com}
12
+ s.files = ["CHANGELOG.rdoc", "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
+ s.has_rdoc = true
14
+ s.homepage = %q{http://github.com/hayesdavis/grackle}
15
+ s.rdoc_options = ["--inline-source", "--charset=UTF-8","--main=README.rdoc"]
16
+ s.extra_rdoc_files = ['README.rdoc']
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{grackle}
19
+ s.rubygems_version = %q{1.3.1}
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.}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_runtime_dependency(%q<json>, [">= 0"])
28
+ s.add_dependency(%q<mime-types>, [">= 0"])
29
+ s.add_dependency(%q<oauth>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<json>, [">= 0"])
32
+ s.add_dependency(%q<mime-types>, [">= 0"])
33
+ s.add_dependency(%q<oauth>, [">= 0"])
34
+ end
35
+ else
36
+ s.add_dependency(%q<json>, [">= 0"])
37
+ s.add_dependency(%q<mime-types>, [">= 0"])
38
+ s.add_dependency(%q<oauth>, [">= 0"])
39
+ end
40
+ end
@@ -0,0 +1,255 @@
1
+ module Grackle
2
+
3
+ #Returned by methods which retrieve data from the API
4
+ class TwitterStruct < OpenStruct
5
+ attr_accessor :id
6
+ end
7
+
8
+ #Raised by methods which call the API if a non-200 response status is received
9
+ class TwitterError < StandardError
10
+ attr_accessor :method, :request_uri, :status, :response_body, :response_object
11
+
12
+ def initialize(method, request_uri, status, response_body, msg=nil)
13
+ self.method = method
14
+ self.request_uri = request_uri
15
+ self.status = status
16
+ self.response_body = response_body
17
+ super(msg||"#{self.method} #{self.request_uri} => #{self.status}: #{self.response_body}")
18
+ end
19
+ end
20
+
21
+ # The Client is the public interface to Grackle. You build Twitter API calls using method chains. See the README for details
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_secret=>'SOMECONSUMERTOKENFROMTWITTER',
43
+ # :token=>'ACCESSTOKENACQUIREDONUSERSBEHALF', :token_secret=>'SUPERSECRETACCESSTOKENSECRET'
44
+ # })
45
+ class Client
46
+
47
+ class Request #:nodoc:
48
+ attr_accessor :client, :path, :method, :api, :ssl
49
+
50
+ def initialize(client,api=:rest,ssl=true)
51
+ self.client = client
52
+ self.api = api
53
+ self.ssl = ssl
54
+ self.method = :get
55
+ self.path = ''
56
+ end
57
+
58
+ def <<(path)
59
+ self.path << path
60
+ end
61
+
62
+ def path?
63
+ path.length > 0
64
+ end
65
+
66
+ def url
67
+ "#{scheme}://#{host}#{path}"
68
+ end
69
+
70
+ def host
71
+ client.api_hosts[api]
72
+ end
73
+
74
+ def scheme
75
+ ssl ? 'https' :'http'
76
+ end
77
+ end
78
+
79
+ VALID_METHODS = [:get,:post,:put,:delete]
80
+ VALID_FORMATS = [:json,:xml,:atom,:rss]
81
+
82
+ # Contains the mapping of API name symbols to actual host (and path)
83
+ # prefixes to use with requests. You can add your own to this hash and
84
+ # refer to it wherever Grackle::Client uses an API symbol. You may wish
85
+ # to do this when Twitter introduces API versions greater than 1.
86
+ TWITTER_API_HOSTS = {
87
+ :rest=>'twitter.com',
88
+ :search=>'search.twitter.com',
89
+ :v1=>'api.twitter.com/1'
90
+ }
91
+
92
+ #Basic OAuth information needed to communicate with Twitter
93
+ TWITTER_OAUTH_SPEC = {
94
+ :request_token_path=>'/oauth/request_token',
95
+ :access_token_path=>'/oauth/access_token',
96
+ :authorize_path=>'/oauth/authorize'
97
+ }
98
+
99
+ attr_accessor :auth, :handlers, :default_format, :headers, :ssl, :api, :transport, :request, :api_hosts, :timeout
100
+
101
+ # Arguments (all are optional):
102
+ # - :username - twitter username to authenticate with (deprecated in favor of :auth arg)
103
+ # - :password - twitter password to authenticate with (deprecated in favor of :auth arg)
104
+ # - :handlers - Hash of formats to Handler instances (e.g. {:json=>CustomJSONHandler.new})
105
+ # - :default_format - Symbol of format to use when no format is specified in an API call (e.g. :json, :xml)
106
+ # - :headers - Hash of string keys and values for headers to pass in the HTTP request to twitter
107
+ # - :ssl - true or false to turn SSL on or off. Default is off (i.e. http://)
108
+ # - :api - one of :rest, :search or :v1. :rest is the default
109
+ # - :auth - Hash of authentication type and credentials. Must have :type key with value one of :basic or :oauth
110
+ # - :type=>:basic - Include :username and :password keys
111
+ # - :type=>:oauth - Include :consumer_key, :consumer_secret, :token and :token_secret keys
112
+ def initialize(options={})
113
+ self.transport = Transport.new
114
+ self.handlers = {:json=>Handlers::JSONHandler.new,:xml=>Handlers::XMLHandler.new,:unknown=>Handlers::StringHandler.new}
115
+ self.handlers.merge!(options[:handlers]||{})
116
+ self.default_format = options[:default_format] || :json
117
+ self.headers = {"User-Agent"=>"Grackle/#{Grackle::VERSION}"}.merge!(options[:headers]||{})
118
+ self.ssl = options[:ssl] == true
119
+ self.api = options[:api] || :rest
120
+ self.api_hosts = TWITTER_API_HOSTS.clone
121
+ self.timeout = options[:timeout] || 60
122
+ self.auth = {}
123
+ if options.has_key?(:username) || options.has_key?(:password)
124
+ #Use basic auth if :username and :password args are passed in
125
+ self.auth.merge!({:type=>:basic,:username=>options[:username],:password=>options[:password]})
126
+ end
127
+ #Handle auth mechanism that permits basic or oauth
128
+ if options.has_key?(:auth)
129
+ self.auth = options[:auth]
130
+ if auth[:type] == :oauth
131
+ self.auth = TWITTER_OAUTH_SPEC.merge(auth)
132
+ end
133
+ end
134
+ end
135
+
136
+ def method_missing(name,*args)
137
+ #If method is a format name, execute using that format
138
+ if format_invocation?(name)
139
+ return call_with_format(name,*args)
140
+ end
141
+ #If method ends in ! or ? use that to determine post or get
142
+ if name.to_s =~ /^(.*)(!|\?)$/
143
+ name = $1.to_sym
144
+ #! is a post, ? is a get
145
+ self.request.method = ($2 == '!' ? :post : :get)
146
+ if format_invocation?(name)
147
+ return call_with_format(name,*args)
148
+ else
149
+ self.request << "/#{$1}"
150
+ return call_with_format(self.default_format,*args)
151
+ end
152
+ end
153
+ #Else add to the request path
154
+ self.request << "/#{name}"
155
+ self
156
+ end
157
+
158
+ # Used to toggle APIs for a particular request without setting the Client's default API
159
+ # client[:rest].users.show.hayesdavis?
160
+ def [](api_name)
161
+ request.api = api_name
162
+ self
163
+ end
164
+
165
+ #Clears any pending request built up by chained methods but not executed
166
+ def clear
167
+ self.request = nil
168
+ end
169
+
170
+ #Deprecated in favor of using the auth attribute.
171
+ def username
172
+ if auth[:type] == :basic
173
+ auth[:username]
174
+ end
175
+ end
176
+
177
+ #Deprecated in favor of using the auth attribute.
178
+ def username=(value)
179
+ unless auth[:type] == :basic
180
+ auth[:type] = :basic
181
+ end
182
+ auth[:username] = value
183
+ end
184
+
185
+ #Deprecated in favor of using the auth attribute.
186
+ def password
187
+ if auth[:type] == :basic
188
+ auth[:password]
189
+ end
190
+ end
191
+
192
+ #Deprecated in favor of using the auth attribute.
193
+ def password=(value)
194
+ unless auth[:type] == :basic
195
+ auth[:type] = :basic
196
+ end
197
+ auth[:password] = value
198
+ end
199
+
200
+ protected
201
+ def call_with_format(format,params={})
202
+ id = params.delete(:id)
203
+ request << "/#{id}" if id
204
+ request << ".#{format}"
205
+ res = send_request(params)
206
+ process_response(format,res)
207
+ ensure
208
+ clear
209
+ end
210
+
211
+ def send_request(params)
212
+ begin
213
+ transport.request(
214
+ request.method,request.url,:auth=>auth,:headers=>headers,:params=>params, :timeout => timeout
215
+ )
216
+ rescue => e
217
+ puts e
218
+ raise TwitterError.new(request.method,request.url,nil,nil,"Unexpected failure making request: #{e}")
219
+ end
220
+ end
221
+
222
+ def process_response(format,res)
223
+ fmt_handler = handler(format)
224
+ begin
225
+ unless res.status == 200
226
+ handle_error_response(res,fmt_handler)
227
+ else
228
+ fmt_handler.decode_response(res.body)
229
+ end
230
+ rescue TwitterError => e
231
+ raise e
232
+ rescue => e
233
+ raise TwitterError.new(res.method,res.request_uri,res.status,res.body,"Unable to decode response: #{e}")
234
+ end
235
+ end
236
+
237
+ def request
238
+ @request ||= Request.new(self,api,ssl)
239
+ end
240
+
241
+ def handler(format)
242
+ handlers[format] || handlers[:unknown]
243
+ end
244
+
245
+ def handle_error_response(res,handler)
246
+ err = TwitterError.new(res.method,res.request_uri,res.status,res.body)
247
+ err.response_object = handler.decode_response(err.response_body)
248
+ raise err
249
+ end
250
+
251
+ def format_invocation?(name)
252
+ self.request.path? && VALID_FORMATS.include?(name)
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,90 @@
1
+ module Grackle
2
+
3
+ # This module contain handlers that know how to take a response body
4
+ # from Twitter and turn it into a TwitterStruct return value. Handlers are
5
+ # used by the Client to give back return values from API calls. A handler
6
+ # is intended to provide a +decode_response+ method which accepts the response body
7
+ # as a string.
8
+ module Handlers
9
+
10
+ # Decodes JSON Twitter API responses
11
+ class JSONHandler
12
+
13
+ def decode_response(res)
14
+ json_result = JSON.parse(res)
15
+ load_recursive(json_result)
16
+ end
17
+
18
+ private
19
+ def load_recursive(value)
20
+ if value.kind_of? Hash
21
+ build_struct(value)
22
+ elsif value.kind_of? Array
23
+ value.map{|v| load_recursive(v)}
24
+ else
25
+ value
26
+ end
27
+ end
28
+
29
+ def build_struct(hash)
30
+ struct = TwitterStruct.new
31
+ hash.each do |key,v|
32
+ struct.send("#{key}=",load_recursive(v))
33
+ end
34
+ struct
35
+ end
36
+
37
+ end
38
+
39
+ # Decodes XML Twitter API responses
40
+ class XMLHandler
41
+
42
+ #Known nodes returned by twitter that contain arrays
43
+ ARRAY_NODES = ['ids','statuses','users']
44
+
45
+ def decode_response(res)
46
+ xml = REXML::Document.new(res)
47
+ load_recursive(xml.root)
48
+ end
49
+
50
+ private
51
+ def load_recursive(node)
52
+ if array_node?(node)
53
+ node.elements.map {|e| load_recursive(e)}
54
+ elsif node.elements.size > 0
55
+ build_struct(node)
56
+ elsif node.elements.size == 0
57
+ value = node.text
58
+ fixnum?(value) ? value.to_i : value
59
+ end
60
+ end
61
+
62
+ def build_struct(node)
63
+ ts = TwitterStruct.new
64
+ node.elements.each do |e|
65
+ ts.send("#{e.name}=",load_recursive(e))
66
+ end
67
+ ts
68
+ end
69
+
70
+ # Most of the time Twitter specifies nodes that contain an array of
71
+ # sub-nodes with a type="array" attribute. There are some nodes that
72
+ # they dont' do that for, though, including the <ids> node returned
73
+ # by the social graph methods. This method tries to work in both situations.
74
+ def array_node?(node)
75
+ node.attributes['type'] == 'array' || ARRAY_NODES.include?(node.name)
76
+ end
77
+
78
+ def fixnum?(value)
79
+ value =~ /^\d+$/
80
+ end
81
+ end
82
+
83
+ # Just echoes back the response body. This is primarily used for unknown formats
84
+ class StringHandler
85
+ def decode_response(res)
86
+ res
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,201 @@
1
+ module Grackle
2
+
3
+ class Response #:nodoc:
4
+ attr_accessor :method, :request_uri, :status, :body
5
+
6
+ def initialize(method,request_uri,status,body)
7
+ self.method = method
8
+ self.request_uri = request_uri
9
+ self.status = status
10
+ self.body = body
11
+ end
12
+ end
13
+
14
+ class Transport
15
+
16
+ attr_accessor :debug
17
+
18
+ CRLF = "\r\n"
19
+ DEFAULT_REDIRECT_LIMIT = 5
20
+
21
+ def req_class(method)
22
+ case method
23
+ when :get then Net::HTTP::Get
24
+ when :post then Net::HTTP::Post
25
+ when :put then Net::HTTP::Put
26
+ when :delete then Net::HTTP::Delete
27
+ end
28
+ end
29
+
30
+ # Options are one of
31
+ # - :params - a hash of parameters to be sent with the request. If a File is a parameter value, \
32
+ # a multipart request will be sent. If a Time is included, .httpdate will be called on it.
33
+ # - :headers - a hash of headers to send with the request
34
+ # - :auth - a hash of authentication parameters for either basic or oauth
35
+ # - :timeout - timeout for the http request in seconds
36
+ def request(method, string_url, options={})
37
+ params = stringify_params(options[:params])
38
+ if method == :get && params
39
+ string_url << query_string(params)
40
+ end
41
+ url = URI.parse(string_url)
42
+ begin
43
+ execute_request(method,url,options)
44
+ rescue Timeout::Error
45
+ raise "Timeout while #{method}ing #{url.to_s}"
46
+ end
47
+ end
48
+
49
+ def execute_request(method,url,options={})
50
+ conn = Net::HTTP.new(url.host, url.port)
51
+ conn.use_ssl = (url.scheme == 'https')
52
+ conn.start do |http|
53
+ req = req_class(method).new(url.request_uri)
54
+ http.read_timeout = options[:timeout]
55
+ add_headers(req,options[:headers])
56
+ if file_param?(options[:params])
57
+ add_multipart_data(req,options[:params])
58
+ else
59
+ add_form_data(req,options[:params])
60
+ end
61
+ if options.has_key? :auth
62
+ if options[:auth][:type] == :basic
63
+ add_basic_auth(req,options[:auth])
64
+ elsif options[:auth][:type] == :oauth
65
+ add_oauth(http,req,options[:auth])
66
+ end
67
+ end
68
+ dump_request(req) if debug
69
+ res = http.request(req)
70
+ dump_response(res) if debug
71
+ redirect_limit = options[:redirect_limit] || DEFAULT_REDIRECT_LIMIT
72
+ if res.code.to_s =~ /^3\d\d$/ && redirect_limit > 0 && res['location']
73
+ execute_request(method,URI.parse(res['location']),options.merge(:redirect_limit=>redirect_limit-1))
74
+ else
75
+ Response.new(method,url.to_s,res.code.to_i,res.body)
76
+ end
77
+ end
78
+ end
79
+
80
+ def query_string(params)
81
+ query = case params
82
+ when Hash then params.map{|key,value| url_encode_param(key,value) }.join("&")
83
+ else url_encode(params.to_s)
84
+ end
85
+ if !(query == nil || query.length == 0) && query[0,1] != '?'
86
+ query = "?#{query}"
87
+ end
88
+ query
89
+ end
90
+
91
+ private
92
+ def stringify_params(params)
93
+ return nil unless params
94
+ params.inject({}) do |h, pair|
95
+ key, value = pair
96
+ if value.respond_to? :httpdate
97
+ value = value.httpdate
98
+ end
99
+ h[key] = value
100
+ h
101
+ end
102
+ end
103
+
104
+ def file_param?(params)
105
+ return false unless params
106
+ params.any? {|key,value| value.respond_to? :read }
107
+ end
108
+
109
+ def url_encode(value)
110
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
111
+ CGI.escape(value.to_s)
112
+ end
113
+
114
+ def url_encode_param(key,value)
115
+ "#{url_encode(key)}=#{url_encode(value)}"
116
+ end
117
+
118
+ def add_headers(req,headers)
119
+ if headers
120
+ headers.each do |header, value|
121
+ req[header] = value
122
+ end
123
+ end
124
+ end
125
+
126
+ def add_form_data(req,params)
127
+ if req.request_body_permitted? && params
128
+ req.set_form_data(params)
129
+ end
130
+ end
131
+
132
+ def add_multipart_data(req,params)
133
+ boundary = Time.now.to_i.to_s(16)
134
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
135
+ body = ""
136
+ params.each do |key,value|
137
+ esc_key = url_encode(key)
138
+ body << "--#{boundary}#{CRLF}"
139
+ if value.respond_to?(:read)
140
+ mime_type = MIME::Types.type_for(value.path)[0] || MIME::Types["application/octet-stream"][0]
141
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
142
+ body << "Content-Type: #{mime_type.simplified}#{CRLF*2}"
143
+ body << value.read
144
+ else
145
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
146
+ end
147
+ body << CRLF
148
+ end
149
+ body << "--#{boundary}--#{CRLF*2}"
150
+ req.body = body
151
+ req["Content-Length"] = req.body.size
152
+ end
153
+
154
+ def add_basic_auth(req,auth)
155
+ username = auth[:username]
156
+ password = auth[:password]
157
+ if username && password
158
+ req.basic_auth(username,password)
159
+ end
160
+ end
161
+
162
+ def add_oauth(conn,req,auth)
163
+ options = auth.reject do |key,value|
164
+ [:type,:consumer_key,:consumer_secret,:token,:token_secret].include?(key)
165
+ end
166
+ unless options.has_key?(:site)
167
+ options[:site] = oauth_site(conn,req)
168
+ end
169
+ consumer = OAuth::Consumer.new(auth[:consumer_key],auth[:consumer_secret],options)
170
+ access_token = OAuth::AccessToken.new(consumer,auth[:token],auth[:token_secret])
171
+ consumer.sign!(req,access_token)
172
+ end
173
+
174
+ private
175
+ def oauth_site(conn,req)
176
+ site = "#{(conn.use_ssl? ? "https" : "http")}://#{conn.address}"
177
+ if (conn.use_ssl? && conn.port != 443) || (!conn.use_ssl? && conn.port != 80)
178
+ site << ":#{conn.port}"
179
+ end
180
+ site
181
+ end
182
+
183
+ def dump_request(req)
184
+ puts "Sending Request"
185
+ puts"#{req.method} #{req.path}"
186
+ dump_headers(req)
187
+ end
188
+
189
+ def dump_response(res)
190
+ puts "Received Response"
191
+ dump_headers(res)
192
+ puts res.body
193
+ end
194
+
195
+ def dump_headers(msg)
196
+ msg.each_header do |key, value|
197
+ puts "\t#{key}=#{value}"
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,16 @@
1
+ module Grackle
2
+ module Utils
3
+
4
+ VALID_PROFILE_IMAGE_SIZES = [:bigger,:normal,:mini]
5
+
6
+ #Easy method for getting different sized profile images using Twitter's naming scheme
7
+ def profile_image_url(url,size=:normal)
8
+ size = VALID_PROFILE_IMAGE_SIZES.find(:normal){|s| s == size.to_sym}
9
+ return url if url.nil? || size == :normal
10
+ url.sub(/_normal\./,"_#{size.to_s}.")
11
+ end
12
+
13
+ module_function :profile_image_url
14
+
15
+ end
16
+ end
data/lib/grackle.rb ADDED
@@ -0,0 +1,31 @@
1
+ module Grackle
2
+
3
+ # :stopdoc:
4
+ VERSION = '0.1.5'
5
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
6
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
7
+ # :startdoc:
8
+
9
+ # Returns the version string for the library.
10
+ def self.version
11
+ VERSION
12
+ end
13
+
14
+ end # module Grackle
15
+
16
+ $:.unshift File.dirname(__FILE__)
17
+
18
+ require 'ostruct'
19
+ require 'open-uri'
20
+ require 'net/http'
21
+ require 'time'
22
+ require 'rexml/document'
23
+ require 'json'
24
+ require 'oauth'
25
+ require 'oauth/client'
26
+ require 'mime/types'
27
+
28
+ require 'grackle/utils'
29
+ require 'grackle/transport'
30
+ require 'grackle/handlers'
31
+ require 'grackle/client'
@@ -0,0 +1,225 @@
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, :last_instance, :responder
9
+ end
10
+
11
+ def request(req)
12
+ self.class.last_instance = self
13
+ if self.class.responder
14
+ self.class.responder.call(self,req)
15
+ else
16
+ self.class.request = req
17
+ self.class.response
18
+ end
19
+ end
20
+ end
21
+
22
+ #Mock responses that conform to HTTPResponse's interface
23
+ class MockResponse < Net::HTTPResponse
24
+ #include Net::HTTPHeader
25
+ attr_accessor :code, :body
26
+ def initialize(code,body,headers={})
27
+ super
28
+ self.code = code
29
+ self.body = body
30
+ headers.each do |name, value|
31
+ self[name] = value
32
+ end
33
+ end
34
+ end
35
+
36
+ #Transport that collects info on requests and responses for testing purposes
37
+ class MockTransport < Grackle::Transport
38
+ attr_accessor :status, :body, :method, :url, :options, :timeout
39
+
40
+ def initialize(status,body,headers={})
41
+ Net::HTTP.response = MockResponse.new(status,body,headers)
42
+ end
43
+
44
+ def request(method, string_url, options)
45
+ self.method = method
46
+ self.url = URI.parse(string_url)
47
+ self.options = options
48
+ super(method,string_url,options)
49
+ end
50
+ end
51
+
52
+ class TestHandler
53
+ attr_accessor :decode_value
54
+
55
+ def initialize(value)
56
+ self.decode_value = value
57
+ end
58
+
59
+ def decode_response(body)
60
+ decode_value
61
+ end
62
+ end
63
+
64
+ def test_redirects
65
+ redirects = 2 #Check that we can follow 2 redirects before getting to original request
66
+ req_count = 0
67
+ responder = Proc.new do |inst, req|
68
+ req_count += 1
69
+ #Store the original request
70
+ if req_count == 1
71
+ inst.class.request = req
72
+ else
73
+ assert_equal("/somewhere_else#{req_count-1}.json",req.path)
74
+ end
75
+ if req_count <= redirects
76
+ MockResponse.new(302,"You are being redirected",'location'=>"http://twitter.com/somewhere_else#{req_count}.json")
77
+ else
78
+ inst.class.response
79
+ end
80
+ end
81
+ with_http_responder(responder) do
82
+ test_simple_get_request
83
+ end
84
+ assert_equal(redirects+1,req_count)
85
+ end
86
+
87
+ def test_timeouts
88
+ client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
89
+ assert_equal(60, client.timeout)
90
+ client.timeout = 30
91
+ assert_equal(30, client.timeout)
92
+ end
93
+
94
+ def test_simple_get_request
95
+ client = new_client(200,'{"id":12345,"screen_name":"test_user"}')
96
+ value = client.users.show.json? :screen_name=>'test_user'
97
+ assert_equal(:get,client.transport.method)
98
+ assert_equal('http',client.transport.url.scheme)
99
+ assert(!Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should not be set to use SSL')
100
+ assert_equal('twitter.com',client.transport.url.host)
101
+ assert_equal('/users/show.json',client.transport.url.path)
102
+ assert_equal('test_user',client.transport.options[:params][:screen_name])
103
+ assert_equal('screen_name=test_user',Net::HTTP.request.path.split(/\?/)[1])
104
+ assert_equal(12345,value.id)
105
+ end
106
+
107
+ def test_simple_post_request_with_basic_auth
108
+ client = Grackle::Client.new(:auth=>{:type=>:basic,:username=>'fake_user',:password=>'fake_pass'})
109
+ test_simple_post(client) do
110
+ assert_match(/Basic/i,Net::HTTP.request['Authorization'],"Request should include Authorization header for basic auth")
111
+ end
112
+ end
113
+
114
+ def test_simple_post_request_with_oauth
115
+ client = Grackle::Client.new(:auth=>{:type=>:oauth,:consumer_key=>'12345',:consumer_secret=>'abc',:token=>'wxyz',:token_secret=>'98765'})
116
+ test_simple_post(client) do
117
+ auth = Net::HTTP.request['Authorization']
118
+ assert_match(/OAuth/i,auth,"Request should include Authorization header for OAuth")
119
+ assert_match(/oauth_consumer_key="12345"/,auth,"Auth header should include consumer key")
120
+ assert_match(/oauth_token="wxyz"/,auth,"Auth header should include token")
121
+ assert_match(/oauth_signature_method="HMAC-SHA1"/,auth,"Auth header should include HMAC-SHA1 signature method as that's what Twitter supports")
122
+ end
123
+ end
124
+
125
+ def test_ssl
126
+ client = new_client(200,'[{"id":1,"text":"test 1"}]',:ssl=>true)
127
+ client.statuses.public_timeline?
128
+ assert_equal("https",client.transport.url.scheme)
129
+ assert(Net::HTTP.last_instance.use_ssl?,'Net::HTTP instance should be set to use SSL')
130
+ end
131
+
132
+ def test_default_format
133
+ client = new_client(200,'[{"id":1,"text":"test 1"}]',:default_format=>:json)
134
+ client.statuses.public_timeline?
135
+ assert_match(/\.json$/,client.transport.url.path)
136
+
137
+ client = new_client(200,'<statuses type="array"><status><id>1</id><text>test 1</text></status></statuses>',:default_format=>:xml)
138
+ client.statuses.public_timeline?
139
+ assert_match(/\.xml$/,client.transport.url.path)
140
+ end
141
+
142
+ def test_api
143
+ client = new_client(200,'[{"id":1,"text":"test 1"}]',:api=>:search)
144
+ client.search? :q=>'test'
145
+ assert_equal('search.twitter.com',client.transport.url.host)
146
+
147
+ client[:rest].users.show.some_user?
148
+ assert_equal('twitter.com',client.transport.url.host)
149
+
150
+ client.api = :search
151
+ client.trends?
152
+ assert_equal('search.twitter.com',client.transport.url.host)
153
+
154
+ client.api = :v1
155
+ client.search? :q=>'test'
156
+ assert_equal('api.twitter.com',client.transport.url.host)
157
+ assert_match(%r{^/1/search},client.transport.url.path)
158
+
159
+ client.api = :rest
160
+ client[:v1].users.show.some_user?
161
+ assert_equal('api.twitter.com',client.transport.url.host)
162
+ assert_match(%r{^/1/users/show/some_user},client.transport.url.path)
163
+ end
164
+
165
+ def test_headers
166
+ client = new_client(200,'[{"id":1,"text":"test 1"}]',:headers=>{'User-Agent'=>'TestAgent/1.0','X-Test-Header'=>'Header Value'})
167
+ client.statuses.public_timeline?
168
+ assert_equal('TestAgent/1.0',Net::HTTP.request['User-Agent'],"Custom User-Agent header should have been set")
169
+ assert_equal('Header Value',Net::HTTP.request['X-Test-Header'],"Custom X-Test-Header header should have been set")
170
+ end
171
+
172
+ def test_custom_handlers
173
+ client = new_client(200,'[{"id":1,"text":"test 1"}]',:handlers=>{:json=>TestHandler.new(42)})
174
+ value = client.statuses.public_timeline.json?
175
+ assert_equal(42,value)
176
+ end
177
+
178
+ def test_clear
179
+ client = new_client(200,'[{"id":1,"text":"test 1"}]')
180
+ client.some.url.that.does.not.exist
181
+ assert_equal('/some/url/that/does/not/exist',client.send(:request).path,"An unexecuted path should be build up")
182
+ client.clear
183
+ assert_equal('',client.send(:request).path,"The path shoudl be cleared")
184
+ end
185
+
186
+ def test_file_param_triggers_multipart_encoding
187
+ client = new_client(200,'[{"id":1,"text":"test 1"}]')
188
+ client.account.update_profile_image! :image=>File.new(__FILE__)
189
+ assert_match(/multipart\/form-data/,Net::HTTP.request['Content-Type'])
190
+ end
191
+
192
+ def test_time_param_is_http_encoded_and_escaped
193
+ client = new_client(200,'[{"id":1,"text":"test 1"}]')
194
+ time = Time.now-60*60
195
+ client.statuses.public_timeline? :since=>time
196
+ assert_equal("/statuses/public_timeline.json?since=#{CGI::escape(time.httpdate)}",Net::HTTP.request.path)
197
+ end
198
+
199
+ private
200
+ def with_http_responder(responder)
201
+ Net::HTTP.responder = responder
202
+ yield
203
+ ensure
204
+ Net::HTTP.responder = nil
205
+ end
206
+
207
+ def new_client(response_status, response_body, client_opts={})
208
+ client = Grackle::Client.new(client_opts)
209
+ client.transport = MockTransport.new(response_status,response_body)
210
+ client
211
+ end
212
+
213
+ def test_simple_post(client)
214
+ client.transport = MockTransport.new(200,'{"id":12345,"text":"test status"}')
215
+ value = client.statuses.update! :status=>'test status'
216
+ assert_equal(:post,client.transport.method,"Expected post request")
217
+ assert_equal('http',client.transport.url.scheme,"Expected scheme to be http")
218
+ assert_equal('twitter.com',client.transport.url.host,"Expected request to be against twitter.com")
219
+ assert_equal('/statuses/update.json',client.transport.url.path)
220
+ assert_match(/status=test%20status/,Net::HTTP.request.body,"Parameters should be form encoded")
221
+ assert_equal(12345,value.id)
222
+ yield(client) if block_given?
223
+ end
224
+
225
+ end
@@ -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 ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grackle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Hayes Davis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-23 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
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
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ description: Grackle is a lightweight library for the Twitter REST and Search API.
46
+ email: hayes@appozite.com
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.rdoc
53
+ files:
54
+ - CHANGELOG.rdoc
55
+ - README.rdoc
56
+ - grackle.gemspec
57
+ - lib/grackle.rb
58
+ - lib/grackle/client.rb
59
+ - lib/grackle/handlers.rb
60
+ - lib/grackle/transport.rb
61
+ - lib/grackle/utils.rb
62
+ - test/test_grackle.rb
63
+ - test/test_helper.rb
64
+ - test/test_client.rb
65
+ - test/test_handlers.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/hayesdavis/grackle
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --inline-source
73
+ - --charset=UTF-8
74
+ - --main=README.rdoc
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ version:
89
+ requirements: []
90
+
91
+ rubyforge_project: grackle
92
+ rubygems_version: 1.3.5
93
+ signing_key:
94
+ specification_version: 2
95
+ 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.
96
+ test_files: []
97
+