grackle 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+