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