almodovar 0.1.0 → 0.1.2

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.
Files changed (31) hide show
  1. data/vendor/resourceful-0.5.3-patched/MIT-LICENSE +21 -0
  2. data/vendor/resourceful-0.5.3-patched/Manifest +29 -0
  3. data/vendor/resourceful-0.5.3-patched/README.markdown +84 -0
  4. data/vendor/resourceful-0.5.3-patched/Rakefile +71 -0
  5. data/vendor/resourceful-0.5.3-patched/lib/resourceful.rb +18 -0
  6. data/vendor/resourceful-0.5.3-patched/lib/resourceful/authentication_manager.rb +108 -0
  7. data/vendor/resourceful-0.5.3-patched/lib/resourceful/cache_manager.rb +240 -0
  8. data/vendor/resourceful-0.5.3-patched/lib/resourceful/exceptions.rb +34 -0
  9. data/vendor/resourceful-0.5.3-patched/lib/resourceful/header.rb +126 -0
  10. data/vendor/resourceful-0.5.3-patched/lib/resourceful/http_accessor.rb +98 -0
  11. data/vendor/resourceful-0.5.3-patched/lib/resourceful/memcache_cache_manager.rb +75 -0
  12. data/vendor/resourceful-0.5.3-patched/lib/resourceful/net_http_adapter.rb +70 -0
  13. data/vendor/resourceful-0.5.3-patched/lib/resourceful/options_interpreter.rb +78 -0
  14. data/vendor/resourceful-0.5.3-patched/lib/resourceful/request.rb +230 -0
  15. data/vendor/resourceful-0.5.3-patched/lib/resourceful/resource.rb +163 -0
  16. data/vendor/resourceful-0.5.3-patched/lib/resourceful/response.rb +221 -0
  17. data/vendor/resourceful-0.5.3-patched/lib/resourceful/stubbed_resource_proxy.rb +47 -0
  18. data/vendor/resourceful-0.5.3-patched/lib/resourceful/util.rb +6 -0
  19. data/vendor/resourceful-0.5.3-patched/resourceful.gemspec +48 -0
  20. data/vendor/resourceful-0.5.3-patched/spec/acceptance/authorization_spec.rb +16 -0
  21. data/vendor/resourceful-0.5.3-patched/spec/acceptance/caching_spec.rb +192 -0
  22. data/vendor/resourceful-0.5.3-patched/spec/acceptance/header_spec.rb +24 -0
  23. data/vendor/resourceful-0.5.3-patched/spec/acceptance/redirecting_spec.rb +12 -0
  24. data/vendor/resourceful-0.5.3-patched/spec/acceptance/resource_spec.rb +84 -0
  25. data/vendor/resourceful-0.5.3-patched/spec/acceptance_shared_specs.rb +44 -0
  26. data/vendor/resourceful-0.5.3-patched/spec/old_acceptance_specs.rb +378 -0
  27. data/vendor/resourceful-0.5.3-patched/spec/simple_sinatra_server.rb +74 -0
  28. data/vendor/resourceful-0.5.3-patched/spec/simple_sinatra_server_spec.rb +98 -0
  29. data/vendor/resourceful-0.5.3-patched/spec/spec.opts +3 -0
  30. data/vendor/resourceful-0.5.3-patched/spec/spec_helper.rb +28 -0
  31. metadata +32 -2
@@ -0,0 +1,230 @@
1
+ require 'pathname'
2
+ require 'benchmark'
3
+ require 'resourceful/response'
4
+ require 'resourceful/net_http_adapter'
5
+ require 'resourceful/exceptions'
6
+
7
+ module Resourceful
8
+
9
+ class Request
10
+
11
+ REDIRECTABLE_METHODS = [:get, :head]
12
+ CACHEABLE_METHODS = [:get, :head]
13
+ INVALIDATING_METHODS = [:post, :put, :delete]
14
+
15
+ attr_accessor :method, :resource, :body, :header
16
+ attr_reader :request_time, :accessor
17
+
18
+ # @param [Symbol] http_method
19
+ # :get, :put, :post, :delete or :head
20
+ # @param [Resourceful::Resource] resource
21
+ # @param [String] body
22
+ # @param [Resourceful::Header, Hash] header
23
+ def initialize(http_method, resource, body = nil, header = nil)
24
+ @method, @resource, @body = http_method, resource, body
25
+ @accessor = @resource.accessor
26
+ @header = header.is_a?(Resourceful::Header) ? header : Resourceful::Header.new(header)
27
+
28
+ # Resourceful handled gzip encoding transparently, so set that up
29
+ @header.accept_encoding ||= 'gzip, identity'
30
+
31
+ # 'Host' is a required HTTP/1.1 header, overrides Host in user-provided headers
32
+ @header.host = @resource.host
33
+
34
+ # Setting the date isn't a bad idea, either
35
+ @header.date ||= Time.now.httpdate
36
+
37
+ # Add any auth credentials we might want
38
+ add_credentials!
39
+
40
+ end
41
+
42
+ # Uses the auth manager to add any valid credentials to this request
43
+ def add_credentials!
44
+ @accessor.auth_manager.add_credentials(self)
45
+ end
46
+
47
+ # Performs all the work. Handles caching, redirects, auth retries, etc
48
+ def fetch_response
49
+ if cached_response
50
+ if needs_revalidation?(cached_response)
51
+ logger.info(" Cache needs revalidation")
52
+ set_validation_headers!(cached_response)
53
+ else
54
+ # We're done!
55
+ return cached_response
56
+ end
57
+ end
58
+
59
+ response = perform!
60
+
61
+ response = revalidate_cached_response(response) if cached_response && response.not_modified?
62
+ response = follow_redirect(response) if should_be_redirected?(response)
63
+ response = retry_with_auth(response) if needs_authorization?(response)
64
+
65
+ raise UnsuccessfulHttpRequestError.new(self, response) if response.error?
66
+
67
+ if cacheable?(response)
68
+ store_in_cache(response)
69
+ elsif invalidates_cache?
70
+ invalidate_cache
71
+ end
72
+
73
+ return response
74
+ end
75
+
76
+ # Should we look for a response to this request in the cache?
77
+ def skip_cache?
78
+ return true unless method.in? CACHEABLE_METHODS
79
+ header.cache_control && header.cache_control.include?('no-cache')
80
+ end
81
+
82
+ # The cached response
83
+ def cached_response
84
+ return if skip_cache?
85
+ return if @cached_response.nil? && @already_checked_cache
86
+ @cached_response ||= begin
87
+ @already_checked_cache = true
88
+ resp = accessor.cache_manager.lookup(self)
89
+ logger.info(" Retrieved from cache") if resp
90
+ resp
91
+ end
92
+ end
93
+
94
+ # Revalidate the cached response with what we got from a 304 response
95
+ def revalidate_cached_response(not_modified_response)
96
+ logger.info(" Resource not modified")
97
+ cached_response.revalidate!(not_modified_response)
98
+ cached_response
99
+ end
100
+
101
+ # Follow a redirect response
102
+ def follow_redirect(response)
103
+ raise MalformedServerResponse.new(self, response) unless response.header.location
104
+ if response.moved_permanently?
105
+ new_uri = response.header.location.first
106
+ logger.info(" Permanently redirected to #{new_uri} - Storing new location.")
107
+ resource.update_uri new_uri
108
+ @header.host = resource.host
109
+ response = fetch_response
110
+ elsif response.see_other? # Always use GET for this redirect, regardless of initial method
111
+ redirected_resource = Resourceful::Resource.new(self.accessor, response.header['Location'].first)
112
+ response = Request.new(:get, redirected_resource, body, header).fetch_response
113
+ else
114
+ redirected_resource = Resourceful::Resource.new(self.accessor, response.header['Location'].first)
115
+ logger.info(" Redirected to #{redirected_resource.uri} - Caching new location.")
116
+ response = Request.new(method, redirected_resource, body, header).fetch_response
117
+ end
118
+ end
119
+
120
+ # Add any auth headers from the response to the auth manager, and try the request again
121
+ def retry_with_auth(response)
122
+ @already_tried_with_auth = true
123
+ logger.info("Authentication Required. Retrying with auth info")
124
+ accessor.auth_manager.associate_auth_info(response)
125
+ add_credentials!
126
+ response = fetch_response
127
+ end
128
+
129
+ # Does this request need to be authorized? Will only be true if we haven't already tried with auth
130
+ def needs_authorization?(response)
131
+ !@already_tried_with_auth && response.unauthorized?
132
+ end
133
+
134
+ # Store the response to this request in the cache
135
+ def store_in_cache(response)
136
+ # RFC2618 - 14.18 : A received message that does not have a Date header
137
+ # field MUST be assigned one by the recipient if the message will be cached
138
+ # by that recipient.
139
+ response.header.date ||= response.response_time.httpdate
140
+
141
+ accessor.cache_manager.store(self, response)
142
+ end
143
+
144
+ # Invalidated the cache for this uri (eg, after a POST)
145
+ def invalidate_cache
146
+ accessor.cache_manager.invalidate(resource.uri)
147
+ end
148
+
149
+ # Is this request & response permitted to be stored in this (private) cache?
150
+ def cacheable?(response)
151
+ return false unless response.success?
152
+ return false unless method.in? CACHEABLE_METHODS
153
+ return false if header.cache_control && header.cache_control.include?('no-store')
154
+ true
155
+ end
156
+
157
+ # Does this request invalidate the cache?
158
+ def invalidates_cache?
159
+ return true if method.in? INVALIDATING_METHODS
160
+ end
161
+
162
+ # Perform the request, with no magic handling of anything.
163
+ def perform!
164
+ @request_time = Time.now
165
+
166
+ http_resp = NetHttpAdapter.make_request(@method, @resource.uri, @body, @header)
167
+ @response = Resourceful::Response.new(uri, *http_resp)
168
+ @response.request_time = @request_time
169
+ @response.authoritative = true
170
+
171
+ @response
172
+ end
173
+
174
+ # Is this a response a redirect, and are we permitted to follow it?
175
+ def should_be_redirected?(response)
176
+ return false unless response.redirect?
177
+ if resource.on_redirect.nil?
178
+ return true if method.in? REDIRECTABLE_METHODS
179
+ false
180
+ else
181
+ resource.on_redirect.call(self, response)
182
+ end
183
+ end
184
+
185
+ # Do we need to revalidate our cache?
186
+ def needs_revalidation?(response)
187
+ return true if forces_revalidation?
188
+ return true if response.stale?
189
+ return true if max_age && response.current_age > max_age
190
+ return true if response.must_be_revalidated?
191
+ false
192
+ end
193
+
194
+ # Set the validation headers of a request based on the response in the cache
195
+ def set_validation_headers!(response)
196
+ @header['If-None-Match'] = response.header['ETag'] if response.header.has_key?('ETag')
197
+ @header['If-Modified-Since'] = response.header['Last-Modified'] if response.header.has_key?('Last-Modified')
198
+ @header['Cache-Control'] = 'max-age=0' if response.must_be_revalidated?
199
+ end
200
+
201
+ # @return [String] The URI against which this request will be, or was, made.
202
+ def uri
203
+ resource.uri
204
+ end
205
+
206
+ # Does this request force us to revalidate the cache?
207
+ def forces_revalidation?
208
+ if max_age == 0 || header.cache_control && cc.include?('no-cache')
209
+ logger.info(" Client forced revalidation")
210
+ true
211
+ else
212
+ false
213
+ end
214
+ end
215
+
216
+ # Indicates the maxmimum response age in seconds we are willing to accept
217
+ #
218
+ # Returns nil if we don't care how old the response is
219
+ def max_age
220
+ if header['Cache-Control'] and header['Cache-Control'].include?('max-age')
221
+ header['Cache-Control'].split(',').grep(/max-age/).first.split('=').last.to_i
222
+ end
223
+ end
224
+
225
+ def logger
226
+ resource.logger
227
+ end
228
+ end
229
+
230
+ end
@@ -0,0 +1,163 @@
1
+ require 'pathname'
2
+ require 'resourceful/request'
3
+
4
+ module Resourceful
5
+
6
+ class Resource
7
+ attr_reader :accessor
8
+ attr_accessor :default_header
9
+
10
+ # Build a new resource for a uri
11
+ #
12
+ # @param accessor<HttpAccessor>
13
+ # The parent http accessor
14
+ # @param uri<String, Addressable::URI>
15
+ # The uri for the location of the resource
16
+ def initialize(accessor, uri, default_header = {})
17
+ @accessor, @uris = accessor, [uri]
18
+ @default_header = Resourceful::Header.new({'User-Agent' => Resourceful::RESOURCEFUL_USER_AGENT_TOKEN}.merge(default_header))
19
+ @on_redirect = nil
20
+ end
21
+
22
+ # The uri used to identify this resource. This is almost always the uri
23
+ # used to create the resource, but in the case of a permanent redirect, this
24
+ # will always reflect the lastest uri.
25
+ #
26
+ # @return Addressable::URI
27
+ # The current uri of the resource
28
+ def effective_uri
29
+ @uris.first
30
+ end
31
+ alias uri effective_uri
32
+
33
+ # Returns the host for this Resource's current uri
34
+ def host
35
+ Addressable::URI.parse(uri).host
36
+ end
37
+
38
+ # Updates the effective uri after following a permanent redirect
39
+ def update_uri(uri)
40
+ @uris.unshift(uri)
41
+ end
42
+
43
+ # When performing a redirect, this callback will be executed first. If the callback
44
+ # returns true, then the redirect is followed, otherwise it is not. The request that
45
+ # triggered the redirect and the response will be passed into the block. This can be
46
+ # used to update any links on the client side.
47
+ #
48
+ # Example:
49
+ #
50
+ # author_resource.on_redirect do |req, resp|
51
+ # post.author_uri = resp.header['Location']
52
+ # end
53
+ #
54
+ # @yieldparam callback<request, response>
55
+ # The action to be executed when a request results in a redirect. Yields the
56
+ # current request and result objects to the callback.
57
+ #
58
+ # @raise ArgumentError if called without a block
59
+ def on_redirect(&callback)
60
+ if block_given?
61
+ @on_redirect = callback
62
+ else
63
+ @on_redirect
64
+ end
65
+ end
66
+
67
+ # Performs a GET on the resource, following redirects as neccessary, and retriving
68
+ # it from the local cache if its available and valid.
69
+ #
70
+ # @return [Response] The Response to the final request made.
71
+ #
72
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
73
+ # success, ie the final request returned a 2xx response code
74
+ def get(header = {})
75
+ request(:get, nil, header)
76
+ end
77
+
78
+ # :call-seq:
79
+ # post(data = "", :content_type => mime_type)
80
+ #
81
+ # Performs a POST with the given data to the resource, following redirects as
82
+ # neccessary.
83
+ #
84
+ # @param [String] data
85
+ # The body of the data to be posted
86
+ # @param [Hash] options
87
+ # Options to pass into the request header. At the least, :content_type is required.
88
+ #
89
+ # @return [Response] The Response to the final request that was made.
90
+ #
91
+ # @raise [ArgumentError] unless :content-type is specified in options
92
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
93
+ # success, ie the final request returned a 2xx response code
94
+ def post(data = nil, header = {})
95
+ check_content_type_exists(data, header)
96
+ request(:post, data, header)
97
+ end
98
+
99
+ # :call-seq:
100
+ # put(data = "", :content_type => mime_type)
101
+ #
102
+ # Performs a PUT with the given data to the resource, following redirects as
103
+ # neccessary.
104
+ #
105
+ # @param [String] data
106
+ # The body of the data to be posted
107
+ # @param [Hash] options
108
+ # Options to pass into the request header. At the least, :content_type is required.
109
+ #
110
+ # @return [Response] The response to the final request made.
111
+ #
112
+ # @raise [ArgumentError] unless :content-type is specified in options
113
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
114
+ # success, ie the final request returned a 2xx response code
115
+ def put(data, header = {})
116
+ check_content_type_exists(data, header)
117
+ request(:put, data, header)
118
+ end
119
+
120
+ # Performs a DELETE on the resource, following redirects as neccessary.
121
+ #
122
+ # @return <Response>
123
+ #
124
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
125
+ # success, ie the final request returned a 2xx response code
126
+ def delete(header = {})
127
+ request(:delete, nil, header)
128
+ end
129
+
130
+ def logger
131
+ accessor.logger
132
+ end
133
+
134
+ private
135
+
136
+ # Ensures that the request has a content type header
137
+ # TODO Move this to request
138
+ def check_content_type_exists(body, header)
139
+ if body
140
+ raise MissingContentType unless header.has_key?(:content_type) or default_header.has_key?(:content_type)
141
+ end
142
+ end
143
+
144
+ # Actually make the request
145
+ def request(method, data, header)
146
+ log_request_with_time "#{method.to_s.upcase} [#{uri}]" do
147
+ request = Request.new(method, self, data, default_header.merge(header))
148
+ request.fetch_response
149
+ end
150
+ end
151
+
152
+ # Log it took the time to make the request
153
+ def log_request_with_time(msg, indent = 2)
154
+ logger.info(" " * indent + msg)
155
+ result = nil
156
+ time = Benchmark.measure { result = yield }
157
+ logger.info(" " * indent + "-> Returned #{result.code} in %.4fs" % time.real)
158
+ result
159
+ end
160
+
161
+ end
162
+
163
+ end
@@ -0,0 +1,221 @@
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
+ @authoritative = true
73
+ end
74
+
75
+ # Algorithm taken from RCF2616#13.2.3
76
+ def current_age
77
+ age_value = header['Age'] ? header['Age'].first.to_i : 0
78
+ date_value = Time.httpdate(header['Date'].first)
79
+ now = Time.now
80
+
81
+ apparent_age = [0, response_time - date_value].max
82
+ corrected_received_age = [apparent_age, age_value].max
83
+ current_age = corrected_received_age + (response_time - request_time) + (now - response_time)
84
+ end
85
+
86
+ def body
87
+ encoding = header['Content-Encoding'] && header['Content-Encoding'].first
88
+ case encoding
89
+ when nil
90
+ # body is identity encoded; just return it
91
+ @body
92
+ when /^\s*gzip\s*$/i
93
+ gz_in = ::Zlib::GzipReader.new(StringIO.new(@body, 'r'))
94
+ @body = gz_in.read
95
+ gz_in.close
96
+ header.delete('Content-Encoding')
97
+ @body
98
+ else
99
+ raise UnsupportedContentCoding, "Resourceful does not support #{encoding} content coding"
100
+ end
101
+ end
102
+
103
+ CODE_NAMES = {
104
+ 100 => "Continue".freeze,
105
+ 101 => "Switching Protocols".freeze,
106
+
107
+ 200 => "OK".freeze,
108
+ 201 => "Created".freeze,
109
+ 202 => "Accepted".freeze,
110
+ 203 => "Non-Authoritative Information".freeze,
111
+ 204 => "No Content".freeze,
112
+ 205 => "Reset Content".freeze,
113
+ 206 => "Partial Content".freeze,
114
+
115
+ 300 => "Multiple Choices".freeze,
116
+ 301 => "Moved Permanently".freeze,
117
+ 302 => "Found".freeze,
118
+ 303 => "See Other".freeze,
119
+ 304 => "Not Modified".freeze,
120
+ 305 => "Use Proxy".freeze,
121
+ 307 => "Temporary Redirect".freeze,
122
+
123
+ 400 => "Bad Request".freeze,
124
+ 401 => "Unauthorized".freeze,
125
+ 402 => "Payment Required".freeze,
126
+ 403 => "Forbidden".freeze,
127
+ 404 => "Not Found".freeze,
128
+ 405 => "Method Not Allowed".freeze,
129
+ 406 => "Not Acceptable".freeze,
130
+ 407 => "Proxy Authentication Required".freeze,
131
+ 408 => "Request Timeout".freeze,
132
+ 409 => "Conflict".freeze,
133
+ 410 => "Gone".freeze,
134
+ 411 => "Length Required".freeze,
135
+ 412 => "Precondition Failed".freeze,
136
+ 413 => "Request Entity Too Large".freeze,
137
+ 414 => "Request-URI Too Long".freeze,
138
+ 415 => "Unsupported Media Type".freeze,
139
+ 416 => "Requested Range Not Satisfiable".freeze,
140
+ 417 => "Expectation Failed".freeze,
141
+
142
+ 500 => "Internal Server Error".freeze,
143
+ 501 => "Not Implemented".freeze,
144
+ 502 => "Bad Gateway".freeze,
145
+ 503 => "Service Unavailable".freeze,
146
+ 504 => "Gateway Timeout".freeze,
147
+ 505 => "HTTP Version Not Supported".freeze,
148
+ }.freeze
149
+
150
+ CODES = CODE_NAMES.keys
151
+
152
+ CODE_NAMES.each do |code, msg|
153
+ method_name = msg.downcase.gsub(/[- ]/, "_")
154
+
155
+ class_eval <<-RUBY
156
+ def #{method_name}? # def ok?
157
+ @code == #{code} # @code == 200
158
+ end # end
159
+ RUBY
160
+ end
161
+
162
+ # Is the response informational? True for
163
+ # 1xx series response codes
164
+ #
165
+ # @return true|false
166
+ def informational?
167
+ @code.in? 100..199
168
+ end
169
+
170
+ # Is the response code sucessful? True for only 2xx series
171
+ # response codes.
172
+ #
173
+ # @return true|false
174
+ def successful?
175
+ @code.in? 200..299
176
+ end
177
+ alias success? successful?
178
+
179
+ # Is the response a redirect? True for
180
+ # 3xx series response codes
181
+ #
182
+ # @return true|false
183
+ def redirection?
184
+ @code.in? 300..399
185
+ end
186
+
187
+ # Is the response a actual redirect? True for
188
+ # 301, 302, 303, 307 response codes
189
+ #
190
+ # @return true|false
191
+ def redirect?
192
+ @code.in? REDIRECT_RESPONSE_CODES
193
+ end
194
+
195
+ # Is the response the result of a client error? True for
196
+ # 4xx series response codes
197
+ #
198
+ # @return true|false
199
+ def client_error?
200
+ @code.in? 400..499
201
+ end
202
+
203
+ # Is the response the result of a server error? True for
204
+ # 5xx series response codes
205
+ #
206
+ # @return true|false
207
+ def server_error?
208
+ @code.in? 500..599
209
+ end
210
+
211
+ # Is the response the result of any kind of error? True for
212
+ # 4xx and 5xx series response codes
213
+ #
214
+ # @return true|false
215
+ def error?
216
+ server_error? || client_error?
217
+ end
218
+
219
+ end
220
+
221
+ end