DanaDanger-ruby_tubesday 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,212 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'net/https'
4
+ require 'rubygems'
5
+ require 'activesupport'
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
+ # params:: Parameters to include in every request. For example, if
40
+ # the service you're accessing requires an API key, you can
41
+ # set it here and it will be included in every request made
42
+ # from this client.
43
+ # max_redirects:: Maximum number of redirects to follow in a single request
44
+ # before throwing an exception. Default is five.
45
+ # ca_file:: Path to a file containing certifying authority
46
+ # certificates (for verifying SSL server certificates).
47
+ # Default is the CA bundle included with RubyTubesday,
48
+ # which is a copy of the bundle included with CentOS 5.
49
+ # verify_ssl:: Whether to verify SSL certificates. If a certificate
50
+ # fails verification, the request will throw an exception.
51
+ # Default is true.
52
+ # username:: Username to send using basic authentication with every
53
+ # request.
54
+ # password:: Username to send using basic authentication with every
55
+ # request.
56
+ #
57
+ # All of these options can be overriden on a per-request basis.
58
+ def initialize(options = {})
59
+ @default_options = {
60
+ :raw => false,
61
+ :cache => ActiveSupport::Cache::MemoryStore.new,
62
+ :params => {},
63
+ :max_redirects => 5,
64
+ :ca_file => (File.dirname(__FILE__) + '/../ca-bundle.crt'),
65
+ :verify_ssl => true,
66
+ :username => nil,
67
+ :password => nil
68
+ }
69
+ @default_options = normalize_options(options)
70
+ end
71
+
72
+ # Fetches a URL using the GET method. Accepts the same options as new.
73
+ # Options specified here are merged into the options specified when the
74
+ # client was instantiated.
75
+ #
76
+ # Parameters in the URL will be merged with the params option. The params
77
+ # option supercedes parameters specified in the URL. For example:
78
+ #
79
+ # # Fetches http://example.com/search?q=ruby&lang=en
80
+ # http.get 'http://example.com/search?q=ruby', :params => { :lang => 'en' }
81
+ #
82
+ # # Fetches http://example.com/search?q=ruby&lang=ja
83
+ # http.get 'http://example.com/search?q=ruby&lang=en', :params => { :lang => 'ja' }
84
+ #
85
+ def get(url, options = {})
86
+ options = normalize_options(options)
87
+ url = URI.parse(url)
88
+
89
+ url_params = CGI.parse(url.query || '')
90
+ params = url_params.merge(options[:params])
91
+ query_string = ''
92
+ unless params.empty?
93
+ params.each do |key, values|
94
+ values = [values] unless values.is_a?(Array)
95
+ values.each do |value|
96
+ query_string += "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}&"
97
+ end
98
+ end
99
+ query_string.chop!
100
+ url.query = query_string
101
+ query_string = "?#{query_string}"
102
+ end
103
+ request = Net::HTTP::Get.new(url.path + query_string)
104
+
105
+ process_request(request, url, options)
106
+ end
107
+
108
+ # Sends data to a URL using the POST method. Accepts the same options as new.
109
+ # Options specified here are merged into the options specified when the
110
+ # client was instantiated.
111
+ #
112
+ # Parameters in the URL will be ignored. The post body will be URL-encoded.
113
+ #
114
+ # This method never uses the cache.
115
+ #
116
+ def post(url, options = {})
117
+ options = normalize_options(options)
118
+ url = URI.parse(url)
119
+
120
+ request = Net::HTTP::Post.new(url.path)
121
+ request.set_form_data(options[:params])
122
+
123
+ process_request(request, url, options)
124
+ end
125
+
126
+ protected
127
+
128
+ def normalize_options(options) # :nodoc:
129
+ normalized_options = {
130
+ :raw => options.delete(:raw),
131
+ :cache => options.delete(:cache),
132
+ :params => options.delete(:params) || @default_options[:params],
133
+ :max_redirects => options.delete(:max_redirects) || @default_options[:max_redirects],
134
+ :ca_file => options.delete(:ca_file) || @default_options[:ca_file],
135
+ :verify_ssl => options.delete(:verify_ssl),
136
+ :username => options.delete(:username) || @default_options[:username],
137
+ :password => options.delete(:password) || @default_options[:password]
138
+ }
139
+
140
+ normalized_options[:raw] = @default_options[:raw] if normalized_options[:raw].nil?
141
+ normalized_options[:cache] = @default_options[:cache] if normalized_options[:cache].nil?
142
+ normalized_options[:verify_ssl] = @default_options[:verify_ssl] if normalized_options[:verify_ssl].nil?
143
+
144
+ unless options.empty?
145
+ raise ArgumentError, "unrecognized keys: `#{options.keys.join('\', `')}'"
146
+ end
147
+
148
+ normalized_options
149
+ end
150
+
151
+ def process_request(request, url, options) # :nodoc:
152
+ response = nil
153
+ cache_policy_options = CachePolicy.options_for_cache(options[:cache]) || {}
154
+
155
+ # Check the cache first if this is a GET request.
156
+ if request.is_a?(Net::HTTP::Get) && options[:cache] && options[:cache].read(url.to_s)
157
+ response = Marshal.load(options[:cache].read(url.to_s))
158
+ response_age = Time.now - Time.parse(response['Last-Modified'] || response['Date'])
159
+ cache_policy = CachePolicy.new(response['Cache-Control'], cache_policy_options)
160
+ if cache_policy.fetch_action(response_age)
161
+ response = nil
162
+ end
163
+ end
164
+
165
+ # Cache miss. Fetch the entity from the network.
166
+ if response.nil?
167
+ redirects_left = options[:max_redirects]
168
+
169
+ while !response.is_a?(Net::HTTPSuccess)
170
+ client = Net::HTTP.new(url.host, url.port)
171
+
172
+ # Configure authentication.
173
+ if options[:username] && options[:password]
174
+ request.basic_auth options[:username], options[:password]
175
+ end
176
+
177
+ # Configure SSL.
178
+ if (client.use_ssl = url.is_a?(URI::HTTPS))
179
+ client.verify_mode = options[:verify_ssl] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
180
+ client.ca_file = options[:ca_file]
181
+ end
182
+
183
+ # Send the request.
184
+ response = client.start { |w| w.request(request) }
185
+
186
+ if response.is_a?(Net::HTTPRedirection)
187
+ raise(TooManyRedirects) if redirects_left < 1
188
+ url = URI.parse(response['Location'])
189
+ request = Net::HTTP::Get.new(url.path)
190
+ redirects_left -= 1
191
+ elsif !response.is_a?(Net::HTTPSuccess)
192
+ response.error!
193
+ end
194
+ end
195
+
196
+ # Write the response to the cache if we're allowed to.
197
+ if request.is_a?(Net::HTTP::Get) && options[:cache]
198
+ cache_policy = CachePolicy.new(response['Cache-Control'], cache_policy_options)
199
+ if cache_policy.may_cache?
200
+ options[:cache].write(url.to_s, Marshal.dump(response))
201
+ end
202
+ end
203
+ end
204
+
205
+ # Return the response.
206
+ if options[:raw]
207
+ response.body
208
+ else
209
+ RubyTubesday::Parser.parse(response)
210
+ end
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: DanaDanger-ruby_tubesday
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - DanaDanger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-13 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "2.1"
23
+ version:
24
+ description:
25
+ email:
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - lib/ruby_tubesday.rb
34
+ - lib/ruby_tubesday/cache_policy.rb
35
+ - lib/ruby_tubesday/parser.rb
36
+ - ca-bundle.crt
37
+ has_rdoc: true
38
+ homepage: http://github.com/DanaDanger/ruby_tubesday
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.2.0
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: Full-featured HTTP client library.
63
+ test_files: []
64
+