hayesdavis-grackle 0.0.1
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/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
|
+
|