pezra-resourceful 0.5.4

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,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
+