hayesdavis-grackle 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/README.txt +118 -0
- data/Rakefile +35 -0
- data/bin/grackle +8 -0
- data/grackle.gemspec +39 -0
- data/lib/grackle.rb +27 -0
- data/lib/grackle/client.rb +153 -0
- data/lib/grackle/handlers.rb +90 -0
- data/lib/grackle/transport.rb +137 -0
- data/lib/grackle/utils.rb +16 -0
- data/spec/grackle_spec.rb +7 -0
- data/spec/spec_helper.rb +16 -0
- data/test/test_grackle.rb +0 -0
- metadata +95 -0
data/History.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
grackle
|
2
|
+
by Hayes Davis
|
3
|
+
http://www.appozite.com
|
4
|
+
http://hayesdavis.net
|
5
|
+
|
6
|
+
== DESCRIPTION
|
7
|
+
Grackle is a lightweight Ruby wrapper around the Twitter REST and Search APIs. It's based on my experience using the
|
8
|
+
Twitter API to build http://cheaptweet.com. The main goal of Grackle is to never require a release when the Twitter
|
9
|
+
API changes (which it often does) or in the face of a particular Twitter API bug. As such it is somewhat different
|
10
|
+
from other Twitter API libraries. It does not try to hide the Twitter "methods" under an access layer nor does it
|
11
|
+
introduce concrete classes for the various objects returned by Twitter. Instead, calls to the Grackle client map
|
12
|
+
directly to Twitter API URLs. The objects returned by API calls are generated as OpenStructs on the fly and make no
|
13
|
+
assumptions about the presence or absence of any particular attributes. Taking this approach means that changes to
|
14
|
+
URLs used by Twitter, parameters required by those URLs or return values will not require a new release. It
|
15
|
+
will potentially require, however, some modifications to your code that uses Grackle.
|
16
|
+
|
17
|
+
==USING GRACKLE
|
18
|
+
|
19
|
+
===Creating a Grackle::Client
|
20
|
+
require 'grackle'
|
21
|
+
client = Grackle::Client(:username=>'your_user',:password=>'yourpass')
|
22
|
+
|
23
|
+
See Grackle::Client for more information about valid arguments to the constructor including for custom headers and SSL.
|
24
|
+
|
25
|
+
===Grackle Method Syntax
|
26
|
+
Grackle uses a method syntax that corresponds to the Twitter API URLs with a few twists. Where you would have a slash in
|
27
|
+
a Twitter URL, that becomes a "." in a chained set of Grackle method calls. Each call in the method chain is used to build
|
28
|
+
Twitter URL path until a particular call is encountered which causes the request to be sent. Methods which will cause a
|
29
|
+
request to be execute include:
|
30
|
+
*A method call ending in "?" will cause an HTTP GET to be executed
|
31
|
+
*A method call ending in "!" will cause an HTTP POST to be executed
|
32
|
+
*If a valid format such as .json, .xml, .rss or .atom is encounted, a get will be executed with that format
|
33
|
+
*A format method can also include a ? or ! to determine GET or POST in that format respectively
|
34
|
+
|
35
|
+
===GETting Data
|
36
|
+
Invoking the API method "/users/show" in XML format for the Twitter user "some_user" looks like
|
37
|
+
client.users.show.xml :id=>'some_user' #http://twitter.com/users/show.xml?id=some_user
|
38
|
+
|
39
|
+
Or using the JSON format:
|
40
|
+
client.users.show.json :id=>'some_user' #http://twitter.com/users/show.json?id=some_user
|
41
|
+
|
42
|
+
Or, since Twitter also allows certain ids to be part of their URLs, this works:
|
43
|
+
client.users.show.some_user.json #http://twitter.com/users/show/some_user.json
|
44
|
+
|
45
|
+
The client has a default format of :json so if you want to use the above call with the default format you can do:
|
46
|
+
client.users.show.some_user? #http://twitter.com/users/show/some_user.json
|
47
|
+
|
48
|
+
If you include a "?" at the end of any method name, that signals to the Grackle client that you want to execute an HTTP GET request.
|
49
|
+
|
50
|
+
===POSTing data
|
51
|
+
To use Twitter API methods that require an HTTP POST, you need to end your method chain with a bang (!)
|
52
|
+
|
53
|
+
For example, to update the authenticated user's status using the XML format:
|
54
|
+
client.statuses.update.xml! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.xml
|
55
|
+
|
56
|
+
Or, with JSON
|
57
|
+
client.statuses.update.json! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.json
|
58
|
+
|
59
|
+
Or, using the default format
|
60
|
+
client.statuses.update! :status=>'this status is from grackle' #POST to http://twitter.com/statuses/update.json
|
61
|
+
|
62
|
+
===Search
|
63
|
+
Search works the same as everything else but your queries get submitted to search.twitter.com
|
64
|
+
client.search? :q=>'grackle' #http://search.twitter.com/search.json?q=grackle
|
65
|
+
|
66
|
+
===Parameter handling
|
67
|
+
*All parameters are URL encoded as necessary.
|
68
|
+
*If you use a File object as a parameter it will be POSTed to Twitter in a multipart request.
|
69
|
+
*If you use a Time object as a parameter, .httpdate will be called on it and that value will be used
|
70
|
+
|
71
|
+
===Return Values
|
72
|
+
Regardless of the format used, Grackle returns a Grackle::TwitterStruct (which is mostly just an OpenStruct) of data. The attributes
|
73
|
+
available on these structs correspond to the data returned by Twitter.
|
74
|
+
|
75
|
+
===Dealing with Errors
|
76
|
+
If the request to Twitter does not return a status code of 200, then a TwitterError is thrown. This contains the HTTP method used,
|
77
|
+
the full request URI, the response status, the response body in text and a response object build by parsing the formatted error
|
78
|
+
returned by Twitter. It's a good idea to wrap your API calls with rescue clauses for Grackle::TwitterError.
|
79
|
+
|
80
|
+
===Formats
|
81
|
+
Twitter allows you to request data in particular formats. Grackle currently supports JSON and XML. The Grackle::Client has a
|
82
|
+
default_format you can specify. By default, the default_format is :json. If you don't include a named format in your method
|
83
|
+
chain as described above, but use a "?" or "!" then the Grackle::Client.default_format is used.
|
84
|
+
|
85
|
+
== REQUIREMENTS:
|
86
|
+
|
87
|
+
You'll need the following gems to use all features of Grackle:
|
88
|
+
*json_pure
|
89
|
+
*httpclient
|
90
|
+
|
91
|
+
== INSTALL:
|
92
|
+
|
93
|
+
sudo gem install grackle
|
94
|
+
|
95
|
+
== LICENSE:
|
96
|
+
|
97
|
+
(The MIT License)
|
98
|
+
|
99
|
+
Copyright (c) 2009
|
100
|
+
|
101
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
102
|
+
a copy of this software and associated documentation files (the
|
103
|
+
'Software'), to deal in the Software without restriction, including
|
104
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
105
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
106
|
+
permit persons to whom the Software is furnished to do so, subject to
|
107
|
+
the following conditions:
|
108
|
+
|
109
|
+
The above copyright notice and this permission notice shall be
|
110
|
+
included in all copies or substantial portions of the Software.
|
111
|
+
|
112
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
113
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
114
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
115
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
116
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
117
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
118
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
begin
|
10
|
+
load 'tasks/setup.rb'
|
11
|
+
rescue LoadError
|
12
|
+
raise RuntimeError, '### please install the "bones" gem ###'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_in_path 'lib'
|
17
|
+
require 'grackle'
|
18
|
+
|
19
|
+
task :default => 'spec:run'
|
20
|
+
|
21
|
+
PROJ.name = 'grackle'
|
22
|
+
PROJ.authors = 'Hayes Davis'
|
23
|
+
PROJ.email = 'hayes@appozite.com'
|
24
|
+
PROJ.url = 'http://github.com/hayesdavis/grackle'
|
25
|
+
PROJ.version = Grackle::VERSION
|
26
|
+
PROJ.rubyforge.name = 'grackle'
|
27
|
+
PROJ.summary = 'Grackle is a library for the Twitter REST and Search API'
|
28
|
+
PROJ.description = 'Grackle is a library for the Twitter REST and Search API that aims to go with the flow.'
|
29
|
+
PROJ.spec.opts << '--color'
|
30
|
+
PROJ.exclude = %w(.git pkg)
|
31
|
+
|
32
|
+
depend_on 'json'
|
33
|
+
depend_on 'httpclient'
|
34
|
+
|
35
|
+
# EOF
|
data/bin/grackle
ADDED
data/grackle.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{grackle}
|
5
|
+
s.version = "0.0.1"
|
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-03-22}
|
10
|
+
s.description = %q{Grackle is a library for the Twitter REST and Search API that aims to go with the flow.}
|
11
|
+
s.email = %q{hayes@appozite.com}
|
12
|
+
s.files = ["History.txt", "README.txt", "Rakefile", "bin/grackle", "grackle.gemspec", "lib/grackle.rb", "lib/grackle/client.rb", "lib/grackle/handlers.rb", "lib/grackle/transport.rb", "lib/grackle/utils.rb", "spec/grackle_spec.rb", "spec/spec_helper.rb", "test/test_grackle.rb"]
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.homepage = %q{http://github.com/hayesdavis/grackle}
|
15
|
+
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{grackle}
|
18
|
+
s.rubygems_version = %q{1.3.1}
|
19
|
+
s.summary = %q{Grackle is a library for the Twitter REST and Search API}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
27
|
+
s.add_runtime_dependency(%q<httpclient>, [">= 2.1.4"])
|
28
|
+
s.add_development_dependency(%q<bones>, [">= 2.4.2"])
|
29
|
+
else
|
30
|
+
s.add_dependency(%q<json>, [">= 0"])
|
31
|
+
s.add_dependency(%q<httpclient>, [">= 2.1.4"])
|
32
|
+
s.add_dependency(%q<bones>, [">= 2.4.2"])
|
33
|
+
end
|
34
|
+
else
|
35
|
+
s.add_dependency(%q<json>, [">= 0"])
|
36
|
+
s.add_dependency(%q<httpclient>, [">= 2.1.4"])
|
37
|
+
s.add_dependency(%q<bones>, [">= 2.4.2"])
|
38
|
+
end
|
39
|
+
end
|
data/lib/grackle.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Grackle
|
2
|
+
|
3
|
+
# :stopdoc:
|
4
|
+
VERSION = '0.0.1'
|
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 'rexml/document'
|
22
|
+
require 'json'
|
23
|
+
|
24
|
+
require 'grackle/utils'
|
25
|
+
require 'grackle/transport'
|
26
|
+
require 'grackle/handlers'
|
27
|
+
require 'grackle/client'
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Grackle
|
2
|
+
|
3
|
+
class TwitterStruct < OpenStruct
|
4
|
+
attr_accessor :id
|
5
|
+
end
|
6
|
+
|
7
|
+
class TwitterError < StandardError
|
8
|
+
attr_accessor :method, :request_uri, :status, :response_body, :response_object
|
9
|
+
|
10
|
+
def initialize(method, request_uri, status, response_body)
|
11
|
+
self.method = method
|
12
|
+
self.request_uri = request_uri
|
13
|
+
self.status = status
|
14
|
+
self.response_body = response_body
|
15
|
+
super("#{self.method} #{self.request_uri} => #{self.status}: #{self.response_body}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Client
|
20
|
+
|
21
|
+
class Request
|
22
|
+
attr_accessor :path, :method
|
23
|
+
|
24
|
+
def method
|
25
|
+
@method ||= :get
|
26
|
+
end
|
27
|
+
|
28
|
+
def <<(path)
|
29
|
+
self.path << path
|
30
|
+
end
|
31
|
+
|
32
|
+
def path
|
33
|
+
@path ||= ''
|
34
|
+
end
|
35
|
+
|
36
|
+
def path?
|
37
|
+
path.length > 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
VALID_METHODS = [:get,:post,:put,:delete]
|
42
|
+
VALID_FORMATS = [:json,:xml,:atom,:rss]
|
43
|
+
|
44
|
+
REST_API_DOMAIN = 'twitter.com'
|
45
|
+
SEARCH_API_DOMAIN = 'search.twitter.com'
|
46
|
+
|
47
|
+
attr_accessor :username, :password, :handlers, :default_format, :headers, :ssl, :transport, :request
|
48
|
+
|
49
|
+
# Arguments (all are optional):
|
50
|
+
# :username - twitter username to authenticate with
|
51
|
+
# :password - twitter password to authenticate with
|
52
|
+
# :handlers - Hash of formats to Handler instances (e.g. {:json=>CustomJSONHandler.new})
|
53
|
+
# :default_format - Symbol of format to use when no format is specified in an API call (e.g. :json)
|
54
|
+
# :headers - Hash of string keys and values for headers to pass in the HTTP request to twitter
|
55
|
+
# :ssl - true or false to turn SSL on or off. Default is off (i.e. http://)
|
56
|
+
def initialize(options={})
|
57
|
+
self.transport = Transport.new
|
58
|
+
self.username = options.delete(:username)
|
59
|
+
self.password = options.delete(:password)
|
60
|
+
self.handlers = {:json=>Handlers::JSONHandler.new,:xml=>Handlers::XMLHandler.new,:unknown=>Handlers::StringHandler.new}
|
61
|
+
self.handlers.merge!(options[:handlers]||{})
|
62
|
+
self.default_format = options[:default_format] || :json
|
63
|
+
self.headers = {'User-Agent'=>'Grackle/1.0'}.merge!(options[:headers]||{})
|
64
|
+
self.ssl = options[:ssl] == true
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(name,*args)
|
68
|
+
#Check for HTTP method and apply it to the request.
|
69
|
+
#Can use this for an explict HTTP method
|
70
|
+
if http_method_invocation?(name)
|
71
|
+
self.request.method = name
|
72
|
+
return self
|
73
|
+
end
|
74
|
+
#If method is a format name, execute using that format
|
75
|
+
if format_invocation?(name)
|
76
|
+
return call_with_format(name,*args)
|
77
|
+
end
|
78
|
+
#If method ends in ! or ? use that to determine post or get
|
79
|
+
if name.to_s =~ /^(.*)(!|\?)$/
|
80
|
+
name = $1.to_sym
|
81
|
+
#! is a post, ? is a get
|
82
|
+
self.request.method = ($2 == '!' ? :post : :get)
|
83
|
+
if format_invocation?(name)
|
84
|
+
return call_with_format(name,*args)
|
85
|
+
else
|
86
|
+
self.request << "/#{$1}"
|
87
|
+
return call_with_format(self.default_format,*args)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
#Else add to the request path
|
91
|
+
self.request << "/#{name}"
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def rest_api_domain
|
97
|
+
REST_API_DOMAIN
|
98
|
+
end
|
99
|
+
|
100
|
+
def search_api_domain
|
101
|
+
SEARCH_API_DOMAIN
|
102
|
+
end
|
103
|
+
|
104
|
+
def call_with_format(format,params={})
|
105
|
+
id = params.delete(:id)
|
106
|
+
self.request << "/#{id}" if id
|
107
|
+
self.request << ".#{format}"
|
108
|
+
url = "#{scheme}://#{request_host}#{self.request.path}"
|
109
|
+
req_info = self.request
|
110
|
+
self.request = nil
|
111
|
+
res = transport.request(
|
112
|
+
req_info.method,url,:username=>self.username,:password=>self.password,:headers=>headers,:params=>params
|
113
|
+
)
|
114
|
+
fmt_handler = handler(format)
|
115
|
+
unless res.status == 200
|
116
|
+
handle_error_response(res,fmt_handler)
|
117
|
+
else
|
118
|
+
fmt_handler.decode_response(res.body)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def request
|
123
|
+
@request ||= Request.new
|
124
|
+
end
|
125
|
+
|
126
|
+
def handler(format)
|
127
|
+
handlers[format] || handlers[:unknown]
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_error_response(res,handler)
|
131
|
+
err = TwitterError.new(res.method,res.request_uri,res.status,res.body)
|
132
|
+
err.response_object = handler.decode_response(err.response_body)
|
133
|
+
raise err
|
134
|
+
end
|
135
|
+
|
136
|
+
def http_method_invocation?(name)
|
137
|
+
!self.request.path? && VALID_METHODS.include?(name)
|
138
|
+
end
|
139
|
+
|
140
|
+
def format_invocation?(name)
|
141
|
+
self.request.path? && VALID_FORMATS.include?(name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def request_host
|
145
|
+
self.request.path =~ /^\/search/ ? search_api_domain : rest_api_domain
|
146
|
+
end
|
147
|
+
|
148
|
+
def scheme
|
149
|
+
self.ssl ? 'https' :'http'
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
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+ 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,137 @@
|
|
1
|
+
module Grackle
|
2
|
+
|
3
|
+
class Response
|
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
|
+
def get(string_url,options={})
|
17
|
+
request(:get,url,options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(string_url,options={})
|
21
|
+
request(:post,url,options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def put(url,options={})
|
25
|
+
request(:put,url,options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(url,options={})
|
29
|
+
request(:delete,url,options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def req_class(method)
|
33
|
+
case method
|
34
|
+
when :get then Net::HTTP::Get
|
35
|
+
when :post then Net::HTTP::Post
|
36
|
+
when :put then Net::HTTP::Put
|
37
|
+
when :delete then Net::HTTP::Delete
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(method, string_url, options={})
|
42
|
+
params = stringify_params(options[:params])
|
43
|
+
if method == :get && params
|
44
|
+
string_url << query_string(params)
|
45
|
+
end
|
46
|
+
url = URI.parse(string_url)
|
47
|
+
begin
|
48
|
+
if file_param?(options[:params])
|
49
|
+
request_multipart(method,url,options)
|
50
|
+
else
|
51
|
+
request_standard(method,url,options)
|
52
|
+
end
|
53
|
+
rescue Timeout::Error
|
54
|
+
raise "Timeout while #{req.method}ing #{url.to_s}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_multipart(method, url, options={})
|
59
|
+
require 'httpclient' unless defined? HTTPClient
|
60
|
+
client = HTTPClient.new
|
61
|
+
if options[:username] && options[:password]
|
62
|
+
client.set_auth(url.to_s,options.delete(:username),options.delete(:password))
|
63
|
+
end
|
64
|
+
res = client.request(method,url.to_s,nil,options[:params],options[:headers])
|
65
|
+
Response.new(method,url.to_s,res.status,res.content)
|
66
|
+
end
|
67
|
+
|
68
|
+
def request_standard(method,url,options={})
|
69
|
+
Net::HTTP.new(url.host, url.port).start do |http|
|
70
|
+
req = req_class(method).new(url.request_uri)
|
71
|
+
add_headers(req,options[:headers])
|
72
|
+
add_form_data(req,options[:params])
|
73
|
+
add_basic_auth(req,options[:username],options[:password])
|
74
|
+
res = http.request(req)
|
75
|
+
Response.new(method,url.to_s,res.code.to_i,res.body)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def query_string(params)
|
80
|
+
query = case params
|
81
|
+
when Hash then params.map{|key,value| url_encode_param(key,value) }.join("&")
|
82
|
+
else url_encode(params.to_s)
|
83
|
+
end
|
84
|
+
if !(query == nil || query.length == 0) && query[0,1] != '?'
|
85
|
+
query = "?#{query}"
|
86
|
+
end
|
87
|
+
query
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def stringify_params(params)
|
92
|
+
return nil unless params
|
93
|
+
params.inject({}) do |h, pair|
|
94
|
+
key, value = pair
|
95
|
+
if value.respond_to? :httpdate
|
96
|
+
value = value.httpdate
|
97
|
+
end
|
98
|
+
h[key] = value
|
99
|
+
h
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def file_param?(params)
|
104
|
+
return false unless params
|
105
|
+
params.any? {|key,value| value.respond_to? :read }
|
106
|
+
end
|
107
|
+
|
108
|
+
def url_encode(value)
|
109
|
+
require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
|
110
|
+
CGI.escape(value.to_s)
|
111
|
+
end
|
112
|
+
|
113
|
+
def url_encode_param(key,value)
|
114
|
+
"#{url_encode(key)}=#{url_encode(value)}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_headers(req,headers)
|
118
|
+
if headers
|
119
|
+
headers.each do |header, value|
|
120
|
+
req[header] = value
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_form_data(req,params)
|
126
|
+
if req.request_body_permitted? && params
|
127
|
+
req.set_form_data(params)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_basic_auth(req,username,password)
|
132
|
+
if username && password
|
133
|
+
req.basic_auth(username,password)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
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/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require File.expand_path(
|
3
|
+
File.join(File.dirname(__FILE__), %w[.. lib grackle]))
|
4
|
+
|
5
|
+
Spec::Runner.configure do |config|
|
6
|
+
# == Mock Framework
|
7
|
+
#
|
8
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
9
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
10
|
+
#
|
11
|
+
# config.mock_with :mocha
|
12
|
+
# config.mock_with :flexmock
|
13
|
+
# config.mock_with :rr
|
14
|
+
end
|
15
|
+
|
16
|
+
# EOF
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hayesdavis-grackle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hayes Davis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-22 00:00:00 -07: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: httpclient
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.4
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: bones
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.4.2
|
44
|
+
version:
|
45
|
+
description: Grackle is a library for the Twitter REST and Search API that aims to go with the flow.
|
46
|
+
email: hayes@appozite.com
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files: []
|
52
|
+
|
53
|
+
files:
|
54
|
+
- History.txt
|
55
|
+
- README.txt
|
56
|
+
- Rakefile
|
57
|
+
- bin/grackle
|
58
|
+
- grackle.gemspec
|
59
|
+
- lib/grackle.rb
|
60
|
+
- lib/grackle/client.rb
|
61
|
+
- lib/grackle/handlers.rb
|
62
|
+
- lib/grackle/transport.rb
|
63
|
+
- lib/grackle/utils.rb
|
64
|
+
- spec/grackle_spec.rb
|
65
|
+
- spec/spec_helper.rb
|
66
|
+
- test/test_grackle.rb
|
67
|
+
has_rdoc: true
|
68
|
+
homepage: http://github.com/hayesdavis/grackle
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options:
|
71
|
+
- --inline-source
|
72
|
+
- --charset=UTF-8
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: "0"
|
86
|
+
version:
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project: grackle
|
90
|
+
rubygems_version: 1.2.0
|
91
|
+
signing_key:
|
92
|
+
specification_version: 2
|
93
|
+
summary: Grackle is a library for the Twitter REST and Search API
|
94
|
+
test_files: []
|
95
|
+
|