ruby_tubesday 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+