ruby_tubesday 0.3.2
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/ca-bundle.crt +7989 -0
- data/lib/ruby_tubesday.rb +226 -0
- data/lib/ruby_tubesday/cache_policy.rb +85 -0
- data/lib/ruby_tubesday/parser.rb +49 -0
- metadata +77 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/https'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_support'
|
6
|
+
|
7
|
+
require 'ruby_tubesday/parser'
|
8
|
+
require 'ruby_tubesday/cache_policy'
|
9
|
+
|
10
|
+
# RubyTubesday is a full-featured HTTP client. It supports automatic parsing
|
11
|
+
# of content bodies, caching, redirection, verified SSL, and basic
|
12
|
+
# authentication.
|
13
|
+
#
|
14
|
+
# If you have the json gem, RubyTubesday will automatically parse JSON
|
15
|
+
# responses. You can add parsers for other content types with the
|
16
|
+
# RubyTubesday::Parser class.
|
17
|
+
#
|
18
|
+
# == Example
|
19
|
+
#
|
20
|
+
# require 'rubygems'
|
21
|
+
# require 'ruby_tubesday'
|
22
|
+
#
|
23
|
+
# http = RubyTubesday.new :params => { :api_key => '12345', :output => 'json' }
|
24
|
+
# result = http.get 'http://maps.google.com/maps/geo', :params => { :q => '123 Main Street' }
|
25
|
+
# lat_lng = result['Placemark'].first['Point']['coordinates']
|
26
|
+
#
|
27
|
+
class RubyTubesday
|
28
|
+
# Thrown when a request is redirected too many times.
|
29
|
+
class TooManyRedirects < Exception; end
|
30
|
+
|
31
|
+
# Creates a new HTTP client. Accepts the following options:
|
32
|
+
#
|
33
|
+
# raw:: Whether to always return raw content bodies. If this is
|
34
|
+
# false, content types registered with RubyTubesday::Parser
|
35
|
+
# will be automatically parsed for you. Default is false.
|
36
|
+
# cache:: An instance of an ActiveSupport::Cache subclass to use
|
37
|
+
# as a content cache. You can set this to false to disable
|
38
|
+
# caching. Default is a new MemoryStore.
|
39
|
+
# force_cache:: An amount of time to cache requests, regardless of the
|
40
|
+
# request's cache control policy. Default is nil.
|
41
|
+
# params:: Parameters to include in every request. For example, if
|
42
|
+
# the service you're accessing requires an API key, you can
|
43
|
+
# set it here and it will be included in every request made
|
44
|
+
# from this client.
|
45
|
+
# max_redirects:: Maximum number of redirects to follow in a single request
|
46
|
+
# before throwing an exception. Default is five.
|
47
|
+
# ca_file:: Path to a file containing certifying authority
|
48
|
+
# certificates (for verifying SSL server certificates).
|
49
|
+
# Default is the CA bundle included with RubyTubesday,
|
50
|
+
# which is a copy of the bundle included with CentOS 5.
|
51
|
+
# verify_ssl:: Whether to verify SSL certificates. If a certificate
|
52
|
+
# fails verification, the request will throw an exception.
|
53
|
+
# Default is true.
|
54
|
+
# username:: Username to send using basic authentication with every
|
55
|
+
# request.
|
56
|
+
# password:: Username to send using basic authentication with every
|
57
|
+
# request.
|
58
|
+
# headers:: Hash of HTTP headers to set for every request.
|
59
|
+
#
|
60
|
+
# All of these options can be overriden on a per-request basis.
|
61
|
+
def initialize(options = {})
|
62
|
+
@default_options = {
|
63
|
+
:raw => false,
|
64
|
+
:cache => ActiveSupport::Cache::MemoryStore.new,
|
65
|
+
:force_cache => nil,
|
66
|
+
:params => {},
|
67
|
+
:max_redirects => 5,
|
68
|
+
:ca_file => (File.dirname(__FILE__) + '/../ca-bundle.crt'),
|
69
|
+
:verify_ssl => true,
|
70
|
+
:username => nil,
|
71
|
+
:password => nil,
|
72
|
+
:headers => nil
|
73
|
+
}
|
74
|
+
@default_options = normalize_options(options)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fetches a URL using the GET method. Accepts the same options as new.
|
78
|
+
# Options specified here are merged into the options specified when the
|
79
|
+
# client was instantiated.
|
80
|
+
#
|
81
|
+
# Parameters in the URL will be merged with the params option. The params
|
82
|
+
# option supercedes parameters specified in the URL. For example:
|
83
|
+
#
|
84
|
+
# # Fetches http://example.com/search?q=ruby&lang=en
|
85
|
+
# http.get 'http://example.com/search?q=ruby', :params => { :lang => 'en' }
|
86
|
+
#
|
87
|
+
# # Fetches http://example.com/search?q=ruby&lang=ja
|
88
|
+
# http.get 'http://example.com/search?q=ruby&lang=en', :params => { :lang => 'ja' }
|
89
|
+
#
|
90
|
+
def get(url, options = {})
|
91
|
+
options = normalize_options(options)
|
92
|
+
url = URI.parse(url)
|
93
|
+
|
94
|
+
url_params = CGI.parse(url.query || '')
|
95
|
+
params = url_params.merge(options[:params])
|
96
|
+
query_string = ''
|
97
|
+
unless params.empty?
|
98
|
+
params.each do |key, values|
|
99
|
+
values = [values] unless values.is_a?(Array)
|
100
|
+
values.each do |value|
|
101
|
+
query_string += "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}&"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
query_string.chop!
|
105
|
+
url.query = query_string
|
106
|
+
query_string = "?#{query_string}"
|
107
|
+
end
|
108
|
+
request = Net::HTTP::Get.new(url.path + query_string)
|
109
|
+
|
110
|
+
process_request(request, url, options)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Sends data to a URL using the POST method. Accepts the same options as new.
|
114
|
+
# Options specified here are merged into the options specified when the
|
115
|
+
# client was instantiated.
|
116
|
+
#
|
117
|
+
# Parameters in the URL will be ignored. The post body will be URL-encoded.
|
118
|
+
#
|
119
|
+
# This method never uses the cache.
|
120
|
+
#
|
121
|
+
def post(url, options = {})
|
122
|
+
options = normalize_options(options)
|
123
|
+
url = URI.parse(url)
|
124
|
+
|
125
|
+
request = Net::HTTP::Post.new(url.path)
|
126
|
+
request.set_form_data(options[:params])
|
127
|
+
|
128
|
+
process_request(request, url, options)
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
def normalize_options(options) # :nodoc:
|
134
|
+
normalized_options = {
|
135
|
+
:raw => options.delete(:raw),
|
136
|
+
:cache => options.delete(:cache),
|
137
|
+
:force_cache => options.delete(:force_cache),
|
138
|
+
:params => options.delete(:params) || @default_options[:params],
|
139
|
+
:max_redirects => options.delete(:max_redirects) || @default_options[:max_redirects],
|
140
|
+
:ca_file => options.delete(:ca_file) || @default_options[:ca_file],
|
141
|
+
:verify_ssl => options.delete(:verify_ssl),
|
142
|
+
:username => options.delete(:username) || @default_options[:username],
|
143
|
+
:password => options.delete(:password) || @default_options[:password],
|
144
|
+
:headers => options.delete(:headers) || @default_options[:headers]
|
145
|
+
}
|
146
|
+
|
147
|
+
normalized_options[:raw] = @default_options[:raw] if normalized_options[:raw].nil?
|
148
|
+
normalized_options[:cache] = @default_options[:cache] if normalized_options[:cache].nil?
|
149
|
+
normalized_options[:force_cache] = @default_options[:force_cache] if normalized_options[:force_cache].nil?
|
150
|
+
normalized_options[:verify_ssl] = @default_options[:verify_ssl] if normalized_options[:verify_ssl].nil?
|
151
|
+
|
152
|
+
unless options.empty?
|
153
|
+
raise ArgumentError, "unrecognized keys: `#{options.keys.join('\', `')}'"
|
154
|
+
end
|
155
|
+
|
156
|
+
normalized_options
|
157
|
+
end
|
158
|
+
|
159
|
+
def process_request(request, url, options) # :nodoc:
|
160
|
+
response = nil
|
161
|
+
cache_policy_options = CachePolicy.options_for_cache(options[:cache]) || {}
|
162
|
+
|
163
|
+
# Check the cache first if this is a GET request.
|
164
|
+
if request.is_a?(Net::HTTP::Get) && options[:cache] && options[:cache].read(url.to_s)
|
165
|
+
response = Marshal.load(options[:cache].read(url.to_s))
|
166
|
+
response_age = Time.now - Time.parse(response['Last-Modified'] || response['Date'])
|
167
|
+
cache_policy = CachePolicy.new(response['Cache-Control'], cache_policy_options)
|
168
|
+
if (options[:force_cache] && (options[:force_cache] < response_age)) || cache_policy.fetch_action(response_age)
|
169
|
+
response = nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Cache miss. Fetch the entity from the network.
|
174
|
+
if response.nil?
|
175
|
+
redirects_left = options[:max_redirects]
|
176
|
+
|
177
|
+
# Configure headers.
|
178
|
+
headers = options[:headers] || {}
|
179
|
+
headers.each do |key, value|
|
180
|
+
request[key] = value
|
181
|
+
end
|
182
|
+
|
183
|
+
while !response.is_a?(Net::HTTPSuccess)
|
184
|
+
client = Net::HTTP.new(url.host, url.port)
|
185
|
+
|
186
|
+
# Configure authentication.
|
187
|
+
if options[:username] && options[:password]
|
188
|
+
request.basic_auth options[:username], options[:password]
|
189
|
+
end
|
190
|
+
|
191
|
+
# Configure SSL.
|
192
|
+
if (client.use_ssl = url.is_a?(URI::HTTPS))
|
193
|
+
client.verify_mode = options[:verify_ssl] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
194
|
+
client.ca_file = options[:ca_file]
|
195
|
+
end
|
196
|
+
|
197
|
+
# Send the request.
|
198
|
+
response = client.start { |w| w.request(request) }
|
199
|
+
|
200
|
+
if response.is_a?(Net::HTTPRedirection)
|
201
|
+
raise(TooManyRedirects) if redirects_left < 1
|
202
|
+
url = URI.parse(response['Location'])
|
203
|
+
request = Net::HTTP::Get.new(url.path)
|
204
|
+
redirects_left -= 1
|
205
|
+
elsif !response.is_a?(Net::HTTPSuccess)
|
206
|
+
response.error!
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Write the response to the cache if we're allowed to.
|
211
|
+
if request.is_a?(Net::HTTP::Get) && options[:cache]
|
212
|
+
cache_policy = CachePolicy.new(response['Cache-Control'], cache_policy_options)
|
213
|
+
if cache_policy.may_cache? || options[:force_cache]
|
214
|
+
options[:cache].write(url.to_s, Marshal.dump(response))
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Return the response.
|
220
|
+
if options[:raw]
|
221
|
+
response.body
|
222
|
+
else
|
223
|
+
RubyTubesday::Parser.parse(response)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class RubyTubesday
|
2
|
+
class CachePolicy # :nodoc:
|
3
|
+
def initialize(cache_control_header, options = {})
|
4
|
+
# Extract options.
|
5
|
+
@stored = options.delete(:stored)
|
6
|
+
@shared = options.delete(:shared)
|
7
|
+
@stored = false if @stored.nil?
|
8
|
+
@shared = false if @shared.nil?
|
9
|
+
|
10
|
+
unless options.empty?
|
11
|
+
raise ArgumentError, "unrecognized keys: `#{options.keys.join('\', `')}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parse Cache-Control header.
|
15
|
+
if cache_control_header.blank?
|
16
|
+
@must_revalidate = true
|
17
|
+
else
|
18
|
+
directives = cache_control_header.split(',')
|
19
|
+
directives.each do |directive|
|
20
|
+
directive.sub!(/^ +/, '')
|
21
|
+
directive.sub!(/ +$/, '')
|
22
|
+
key, value = directive.split('=', 2)
|
23
|
+
|
24
|
+
case key
|
25
|
+
when 'public' : @privacy = PUBLIC
|
26
|
+
when 'private' : @privacy = PRIVATE
|
27
|
+
when 'no-cache' : @storability = NO_CACHE
|
28
|
+
when 'no-store' : @storability = NO_STORE
|
29
|
+
when 'max-age' : @max_age = value.to_i
|
30
|
+
when 's-maxage' : @s_max_age = value.to_i
|
31
|
+
when 'must-revalidate' : @must_revalidate = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Indicates that a cached response must be refetched. Returned by fetch_action.
|
38
|
+
REFETCH = :refetch
|
39
|
+
# Indicates that a cached response must be revalidated. Returned by fetch_action.
|
40
|
+
REVALIDATE = :revalidate
|
41
|
+
# Indicates that a cached response may be used as-is. Returned by fetch_action.
|
42
|
+
USE_CACHE = nil
|
43
|
+
|
44
|
+
# Returns a constant indicating whether a cached response must be refetched
|
45
|
+
# (returns REFETCH) or revalidated (returns REVALIDATE). Returns USE_CACHE if
|
46
|
+
# the cached response may be used as-is. +response_age+ is the age of the
|
47
|
+
# cached response in seconds.
|
48
|
+
#
|
49
|
+
# If you don't care about the distinction between refetching and
|
50
|
+
# revalidating, you can treat the return value from this method as Boolean.
|
51
|
+
def fetch_action(response_age)
|
52
|
+
return REFETCH if @shared && @s_max_age && (response_age > @s_max_age)
|
53
|
+
return REFETCH if @max_age && (response_age > @max_age)
|
54
|
+
return REVALIDATE if @must_revalidate
|
55
|
+
USE_CACHE
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a Boolean value indicating whether a response is allowed to be
|
59
|
+
# cached according to this cache policy.
|
60
|
+
def may_cache?
|
61
|
+
return false if @storability == NO_CACHE
|
62
|
+
return false if @stored && @storability == NO_STORE
|
63
|
+
return false if @shared && @privacy == PRIVATE
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.options_for_cache(cache)
|
68
|
+
case cache
|
69
|
+
when ActiveSupport::Cache::MemoryStore : { :shared => false, :stored => false }
|
70
|
+
when ActiveSupport::Cache::FileStore : { :shared => true, :stored => true }
|
71
|
+
when ActiveSupport::Cache::MemCacheStore : { :shared => true, :stored => false }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
# Privacy levels
|
78
|
+
PUBLIC = :public
|
79
|
+
PRIVATE = :private
|
80
|
+
|
81
|
+
# Storability levels
|
82
|
+
NO_CACHE = :no_cache
|
83
|
+
NO_STORE = :no_store
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class RubyTubesday
|
2
|
+
# Handles automatic parsing of responses for RubyTubesday.
|
3
|
+
class Parser
|
4
|
+
# Registers a parser method for a particular content type. When a
|
5
|
+
# RubeTubesday instance receives a response with a registered content type,
|
6
|
+
# the response body is passed to the associated parser method. The method
|
7
|
+
# should return a parsed representation of the response body. For example,
|
8
|
+
# the JSON parser is registered like so:
|
9
|
+
#
|
10
|
+
# RubyTubesday::Parser.register(JSON.method(:parse), 'application/json')
|
11
|
+
#
|
12
|
+
# You can also specify more than one content type:
|
13
|
+
#
|
14
|
+
# RubyTubesday::Parser.register(JSON.method(:parse), 'application/json', 'text/javascript')
|
15
|
+
#
|
16
|
+
# If a parser method is registered for a content type that already has a
|
17
|
+
# parser, the old method is discarded.
|
18
|
+
#
|
19
|
+
def self.register(meth, *mime_types)
|
20
|
+
mime_types.each do |type|
|
21
|
+
@@parser_methods[type] = meth
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse(response) # :nodoc:
|
26
|
+
content_type = response['Content-Type'].split(';').first
|
27
|
+
parser_method = @@parser_methods[content_type]
|
28
|
+
if parser_method
|
29
|
+
parser_method.call(response.body)
|
30
|
+
else
|
31
|
+
response.body
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
@@parser_methods = {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Register a parser for JSON if the json gem is installed.
|
43
|
+
begin
|
44
|
+
require 'rubygems'
|
45
|
+
require 'json'
|
46
|
+
RubyTubesday::Parser.register(JSON.method(:parse), 'application/json')
|
47
|
+
rescue LoadError
|
48
|
+
# Fail silently.
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_tubesday
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 2
|
9
|
+
version: 0.3.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Dana Contreras
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-27 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
version: "2.1"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description:
|
34
|
+
email:
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- lib/ruby_tubesday.rb
|
43
|
+
- lib/ruby_tubesday/cache_policy.rb
|
44
|
+
- lib/ruby_tubesday/parser.rb
|
45
|
+
- ca-bundle.crt
|
46
|
+
has_rdoc: true
|
47
|
+
homepage: http://github.com/dummied/ruby_tubesday
|
48
|
+
licenses: []
|
49
|
+
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.6
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Full-featured HTTP client library.
|
76
|
+
test_files: []
|
77
|
+
|