pezra-resourceful 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+ require 'net/http'
2
+ require 'time'
3
+ require 'zlib'
4
+
5
+ module Resourceful
6
+
7
+ class Response
8
+ REDIRECT_RESPONSE_CODES = [301,302,303,307]
9
+ NORMALLY_CACHEABLE_RESPONSE_CODES = [200, 203, 300, 301, 410]
10
+
11
+ attr_reader :uri, :code, :header, :body, :response_time
12
+ alias headers header
13
+
14
+ attr_accessor :authoritative, :request_time
15
+ alias authoritative? authoritative
16
+
17
+ def initialize(uri, code, header, body)
18
+ @uri, @code, @header, @body = uri, code, header, body
19
+ @response_time = Time.now
20
+ end
21
+
22
+ # Is this a cached response that has expired?
23
+ #
24
+ # @return true|false
25
+ def expired?
26
+ if header['Cache-Control'] and header['Cache-Control'].first.include?('max-age')
27
+ max_age = header['Cache-Control'].first.split(',').grep(/max-age/).first.split('=').last.to_i
28
+ return true if current_age > max_age
29
+ elsif header['Expire']
30
+ return true if Time.httpdate(header['Expire'].first) < Time.now
31
+ end
32
+
33
+ false
34
+ end
35
+
36
+ # Is this a cached response that is stale?
37
+ #
38
+ # @return true|false
39
+ def stale?
40
+ return true if expired?
41
+ if header['Cache-Control']
42
+ return true if header['Cache-Control'].include?('must-revalidate')
43
+ return true if header['Cache-Control'].include?('no-cache')
44
+ end
45
+
46
+ false
47
+ end
48
+
49
+ # Is this response cachable?
50
+ #
51
+ # @return true|false
52
+ def cacheable?
53
+ @cacheable ||= begin
54
+ @cacheable = true if NORMALLY_CACHEABLE_RESPONSE_CODES.include?(code.to_i)
55
+ @cacheable = false if header.vary && header.vary.include?('*')
56
+ @cacheable = false if header.cache_control && header.cache_control.include?('no-cache')
57
+ @cacheable = true if header.cache_control && header.cache_control.include?('public')
58
+ @cacheable = true if header.cache_control && header.cache_control.include?('private')
59
+ @cacheable || false
60
+ end
61
+ end
62
+
63
+ # Does this response force revalidation?
64
+ def must_be_revalidated?
65
+ header.cache_control && header.cache_control.include?('must-revalidate')
66
+ end
67
+
68
+ # Update our headers from a later 304 response
69
+ def revalidate!(not_modified_response)
70
+ header.merge!(not_modified_response.header)
71
+ @request_time = not_modified_response.request_time
72
+ @response_time = not_modified_response.response_time
73
+ @authoritative = true
74
+ end
75
+
76
+ # Algorithm taken from RCF2616#13.2.3
77
+ def current_age
78
+ age_value = header['Age'] ? header['Age'].first.to_i : 0
79
+ date_value = Time.httpdate(header['Date'].first)
80
+ now = Time.now
81
+
82
+ apparent_age = [0, response_time - date_value].max
83
+ corrected_received_age = [apparent_age, age_value].max
84
+ current_age = corrected_received_age + (response_time - request_time) + (now - response_time)
85
+ end
86
+
87
+ def body
88
+ encoding = header['Content-Encoding'] && header['Content-Encoding'].first
89
+ case encoding
90
+ when nil
91
+ # body is identity encoded; just return it
92
+ @body
93
+ when /^\s*gzip\s*$/i
94
+ gz_in = ::Zlib::GzipReader.new(StringIO.new(@body, 'r'))
95
+ @body = gz_in.read
96
+ gz_in.close
97
+ header.delete('Content-Encoding')
98
+ @body
99
+ else
100
+ raise UnsupportedContentCoding, "Resourceful does not support #{encoding} content coding"
101
+ end
102
+ end
103
+
104
+ CODE_NAMES = {
105
+ 100 => "Continue".freeze,
106
+ 101 => "Switching Protocols".freeze,
107
+
108
+ 200 => "OK".freeze,
109
+ 201 => "Created".freeze,
110
+ 202 => "Accepted".freeze,
111
+ 203 => "Non-Authoritative Information".freeze,
112
+ 204 => "No Content".freeze,
113
+ 205 => "Reset Content".freeze,
114
+ 206 => "Partial Content".freeze,
115
+
116
+ 300 => "Multiple Choices".freeze,
117
+ 301 => "Moved Permanently".freeze,
118
+ 302 => "Found".freeze,
119
+ 303 => "See Other".freeze,
120
+ 304 => "Not Modified".freeze,
121
+ 305 => "Use Proxy".freeze,
122
+ 307 => "Temporary Redirect".freeze,
123
+
124
+ 400 => "Bad Request".freeze,
125
+ 401 => "Unauthorized".freeze,
126
+ 402 => "Payment Required".freeze,
127
+ 403 => "Forbidden".freeze,
128
+ 404 => "Not Found".freeze,
129
+ 405 => "Method Not Allowed".freeze,
130
+ 406 => "Not Acceptable".freeze,
131
+ 407 => "Proxy Authentication Required".freeze,
132
+ 408 => "Request Timeout".freeze,
133
+ 409 => "Conflict".freeze,
134
+ 410 => "Gone".freeze,
135
+ 411 => "Length Required".freeze,
136
+ 412 => "Precondition Failed".freeze,
137
+ 413 => "Request Entity Too Large".freeze,
138
+ 414 => "Request-URI Too Long".freeze,
139
+ 415 => "Unsupported Media Type".freeze,
140
+ 416 => "Requested Range Not Satisfiable".freeze,
141
+ 417 => "Expectation Failed".freeze,
142
+
143
+ 500 => "Internal Server Error".freeze,
144
+ 501 => "Not Implemented".freeze,
145
+ 502 => "Bad Gateway".freeze,
146
+ 503 => "Service Unavailable".freeze,
147
+ 504 => "Gateway Timeout".freeze,
148
+ 505 => "HTTP Version Not Supported".freeze,
149
+ }.freeze
150
+
151
+ CODES = CODE_NAMES.keys
152
+
153
+ CODE_NAMES.each do |code, msg|
154
+ method_name = msg.downcase.gsub(/[- ]/, "_")
155
+
156
+ class_eval <<-RUBY
157
+ def #{method_name}? # def ok?
158
+ @code == #{code} # @code == 200
159
+ end # end
160
+ RUBY
161
+ end
162
+
163
+ # Is the response informational? True for
164
+ # 1xx series response codes
165
+ #
166
+ # @return true|false
167
+ def informational?
168
+ @code.in? 100..199
169
+ end
170
+
171
+ # Is the response code sucessful? True for only 2xx series
172
+ # response codes.
173
+ #
174
+ # @return true|false
175
+ def successful?
176
+ @code.in? 200..299
177
+ end
178
+ alias success? successful?
179
+
180
+ # Is the response a redirect? True for
181
+ # 3xx series response codes
182
+ #
183
+ # @return true|false
184
+ def redirection?
185
+ @code.in? 300..399
186
+ end
187
+
188
+ # Is the response a actual redirect? True for
189
+ # 301, 302, 303, 307 response codes
190
+ #
191
+ # @return true|false
192
+ def redirect?
193
+ @code.in? REDIRECT_RESPONSE_CODES
194
+ end
195
+
196
+ # Is the response the result of a client error? True for
197
+ # 4xx series response codes
198
+ #
199
+ # @return true|false
200
+ def client_error?
201
+ @code.in? 400..499
202
+ end
203
+
204
+ # Is the response the result of a server error? True for
205
+ # 5xx series response codes
206
+ #
207
+ # @return true|false
208
+ def server_error?
209
+ @code.in? 500..599
210
+ end
211
+
212
+ # Is the response the result of any kind of error? True for
213
+ # 4xx and 5xx series response codes
214
+ #
215
+ # @return true|false
216
+ def error?
217
+ server_error? || client_error?
218
+ end
219
+
220
+ end
221
+
222
+ end
@@ -0,0 +1,47 @@
1
+ require 'resourceful/resource'
2
+
3
+ module Resourceful
4
+ class StubbedResourceProxy
5
+ def initialize(resource, canned_responses)
6
+ @resource = resource
7
+
8
+ @canned_responses = {}
9
+
10
+ canned_responses.each do |cr|
11
+ mime_type = cr[:mime_type]
12
+ @canned_responses[mime_type] = resp = Net::HTTPOK.new('1.1', '200', 'OK')
13
+ resp['content-type'] = mime_type.to_str
14
+ resp.instance_variable_set(:@read, true)
15
+ resp.instance_variable_set(:@body, cr[:body])
16
+
17
+ end
18
+ end
19
+
20
+ def get_body(*args)
21
+ get(*args).body
22
+ end
23
+
24
+ def get(*args)
25
+ options = args.last.is_a?(Hash) ? args.last : {}
26
+
27
+ if accept = [(options[:accept] || '*/*')].flatten.compact
28
+ accept.each do |mt|
29
+ return canned_response(mt) || next
30
+ end
31
+ @resource.get(*args)
32
+ end
33
+ end
34
+
35
+ def method_missing(method, *args)
36
+ @resource.send(method, *args)
37
+ end
38
+
39
+ protected
40
+
41
+ def canned_response(mime_type)
42
+ mime_type = @canned_responses.keys.first if mime_type == '*/*'
43
+ @canned_responses[mime_type]
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+
2
+ class Object
3
+ def in?(arr)
4
+ arr.include?(self)
5
+ end
6
+ end
@@ -0,0 +1,49 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{resourceful}
5
+ s.version = "0.5.4"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Paul Sadauskas"]
9
+ s.date = %q{2009-08-07}
10
+ s.description = %q{An HTTP library for Ruby that takes advantage of everything HTTP has to offer.}
11
+ s.email = %q{psadauskas@gmail.com}
12
+ s.extra_rdoc_files = ["lib/resourceful/authentication_manager.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/header.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/multipart_form_data.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/options_interpretation.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/response.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/util.rb", "lib/resourceful.rb", "README.markdown"]
13
+ s.files = ["lib/resourceful/authentication_manager.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/header.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/multipart_form_data.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/options_interpretation.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/response.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/util.rb", "lib/resourceful.rb", "Manifest", "MIT-LICENSE", "Rakefile", "README.markdown", "resourceful.gemspec", "spec/acceptance/authorization_spec.rb", "spec/acceptance/caching_spec.rb", "spec/acceptance/header_spec.rb", "spec/acceptance/redirecting_spec.rb", "spec/acceptance/resource_spec.rb", "spec/acceptance_shared_specs.rb", "spec/caching_spec.rb", "spec/old_acceptance_specs.rb", "spec/resourceful/multipart_form_data_spec.rb", "spec/resourceful/resource_spec.rb", "spec/simple_sinatra_server.rb", "spec/simple_sinatra_server_spec.rb", "spec/spec.opts", "spec/spec_helper.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/paul/resourceful}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Resourceful", "--main", "README.markdown"]
17
+ s.require_paths = ["lib", "ext"]
18
+ s.rubyforge_project = %q{resourceful}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{An HTTP library for Ruby that takes advantage of everything HTTP has to offer.}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_runtime_dependency(%q<addressable>, [">= 2.1.0"])
28
+ s.add_runtime_dependency(%q<httpauth>, [">= 0"])
29
+ s.add_development_dependency(%q<thin>, [">= 0"])
30
+ s.add_development_dependency(%q<yard>, [">= 0"])
31
+ s.add_development_dependency(%q<sinatra>, [">= 0"])
32
+ s.add_development_dependency(%q<rspec>, [">= 0"])
33
+ else
34
+ s.add_dependency(%q<addressable>, [">= 2.1.0"])
35
+ s.add_dependency(%q<httpauth>, [">= 0"])
36
+ s.add_dependency(%q<thin>, [">= 0"])
37
+ s.add_dependency(%q<yard>, [">= 0"])
38
+ s.add_dependency(%q<sinatra>, [">= 0"])
39
+ s.add_dependency(%q<rspec>, [">= 0"])
40
+ end
41
+ else
42
+ s.add_dependency(%q<addressable>, [">= 2.1.0"])
43
+ s.add_dependency(%q<httpauth>, [">= 0"])
44
+ s.add_dependency(%q<thin>, [">= 0"])
45
+ s.add_dependency(%q<yard>, [">= 0"])
46
+ s.add_dependency(%q<sinatra>, [">= 0"])
47
+ s.add_dependency(%q<rspec>, [">= 0"])
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require File.dirname(__FILE__) + '/../spec_helper'
3
+ require 'resourceful'
4
+
5
+ describe Resourceful do
6
+
7
+ describe "basic auth" do
8
+
9
+ end
10
+
11
+ describe "digest auth" do
12
+
13
+ end
14
+
15
+ end
16
+
@@ -0,0 +1,190 @@
1
+
2
+ require File.dirname(__FILE__) + '/../spec_helper'
3
+ require 'resourceful'
4
+ require 'addressable/template'
5
+
6
+ describe Resourceful do
7
+
8
+ describe "caching" do
9
+
10
+ before do
11
+ @http = Resourceful::HttpAccessor.new(:cache_manager => Resourceful::InMemoryCacheManager.new)
12
+ if ENV['SPEC_LOGGING']
13
+ @http.logger = Resourceful::StdOutLogger.new
14
+ end
15
+ end
16
+
17
+ def get_with_errors(resource)
18
+ begin
19
+ resp = resource.get
20
+ rescue Resourceful::UnsuccessfulHttpRequestError => e
21
+ resp = e.http_response
22
+ end
23
+ resp
24
+ end
25
+
26
+ def uri_plus_params(uri, params = {})
27
+ uri = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
28
+ uri.query_values = params
29
+ uri
30
+ end
31
+
32
+ def uri_for_code(code, params = {})
33
+ uri = Addressable::Template.new("http://localhost:42682/code/{code}").expand("code" => code.to_s)
34
+ uri_plus_params(uri, params)
35
+ end
36
+
37
+ describe "response cacheability" do
38
+ Resourceful::Response::NORMALLY_CACHEABLE_RESPONSE_CODES.each do |code|
39
+ describe "response code #{code}" do
40
+ it "should normally be cached" do
41
+ resource = @http.resource(uri_for_code(code))
42
+
43
+ resp = get_with_errors(resource)
44
+ resp.should be_cacheable
45
+ end
46
+
47
+ it "should not be cached if Vary: *" do
48
+ resource = @http.resource(uri_for_code(200, "Vary" => "*"))
49
+
50
+ resp = get_with_errors(resource)
51
+ resp.should_not be_cacheable
52
+ end
53
+
54
+ it "should not be cached if Cache-Control: no-cache'" do
55
+ resource = @http.resource(uri_for_code(200, "Cache-Control" => "no-cache"))
56
+
57
+ resp = get_with_errors(resource)
58
+ resp.should_not be_cacheable
59
+ end
60
+ end
61
+ end
62
+
63
+ # I would prefer to do all other codes, but some of them do some magic stuff (100),
64
+ # so I'll just spot check.
65
+ [201, 206, 302, 307, 404, 500].each do |code|
66
+ describe "response code #{code}" do
67
+ it "should not normally be cached" do
68
+ resource = @http.resource(uri_for_code(code))
69
+
70
+ resp = get_with_errors(resource)
71
+ resp.should_not be_cacheable
72
+ end
73
+
74
+ it "should be cached if Cache-Control: public" do
75
+ resource = @http.resource(uri_for_code(code, "Cache-Control" => "public"))
76
+
77
+ resp = get_with_errors(resource)
78
+ resp.should be_cacheable
79
+ end
80
+
81
+ it "should be cached if Cache-Control: private" do
82
+ resource = @http.resource(uri_for_code(code, "Cache-Control" => "private"))
83
+
84
+ resp = get_with_errors(resource)
85
+ resp.should be_cacheable
86
+ end
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ describe "expiration" do
93
+ it 'should use the cached response if Expire: is in the future' do
94
+ in_the_future = (Time.now + 60).httpdate
95
+ resource = @http.resource(uri_for_code(200, "Expire" => in_the_future))
96
+
97
+ resp = resource.get
98
+ resp.should_not be_expired
99
+
100
+ resp = resource.get
101
+ resp.should be_ok
102
+ resp.should_not be_authoritative
103
+ end
104
+
105
+ it 'should revalidate the cached response if the response is expired' do
106
+ in_the_past = (Time.now - 60).httpdate
107
+ resource = @http.resource(uri_for_code(200, "Expire" => in_the_past))
108
+
109
+ resp = resource.get
110
+ resp.should be_expired
111
+
112
+ resp = resource.get
113
+ resp.should be_ok
114
+ resp.should be_authoritative
115
+ end
116
+ end
117
+
118
+ describe 'authoritative' do
119
+
120
+ it "should be authoritative if the response is directly from the server" do
121
+ resource = @http.resource(
122
+ uri_plus_params('http://localhost:42682/', "Cache-Control" => 'max-age=10')
123
+ )
124
+
125
+ response = resource.get
126
+ response.should be_authoritative
127
+ end
128
+
129
+ it "should be authoritative if a cached response was revalidated with the server" do
130
+ now = Time.now.httpdate
131
+ resource = @http.resource(
132
+ uri_plus_params('http://localhost:42682/cached',
133
+ "modified" => now,
134
+ "Cache-Control" => 'max-age=0')
135
+ )
136
+
137
+ resource.get
138
+ response = resource.get("Cache-Control" => "max-age=0")
139
+ response.should be_authoritative
140
+ end
141
+
142
+ it "should not be authoritative if the cached response was not revalidated" do
143
+ now = Time.now.httpdate
144
+ resource = @http.resource(
145
+ uri_plus_params('http://localhost:42682/cached',
146
+ "modified" => now,
147
+ "Cache-Control" => 'max-age=10')
148
+ )
149
+
150
+ resource.get
151
+ response = resource.get
152
+ response.should_not be_authoritative
153
+
154
+ end
155
+
156
+ end
157
+
158
+ describe "Not Modified responses" do
159
+ before do
160
+ now = Time.now.httpdate
161
+
162
+ resource = @http.resource(
163
+ uri_plus_params('http://localhost:42682/cached',
164
+ "modified" => now,
165
+ "Cache-Control" => 'max-age=0')
166
+ )
167
+
168
+ @first_response = resource.get
169
+ @second_response = resource.get("Cache-Control" => "max-age=0") # Force revalidation
170
+ end
171
+
172
+ it "should replace the 304 response with whats in the cache" do
173
+ @second_response.code.should == @first_response.code
174
+ end
175
+
176
+ it "should provide a body identical to the original response" do
177
+ @second_response.body.should == @first_response.body
178
+ end
179
+
180
+ it "should override any cached headers with new ones"
181
+ end
182
+
183
+ describe "cache invalidation" do
184
+
185
+ end
186
+
187
+ end
188
+
189
+ end
190
+