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.
@@ -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
+