lib/wrest/native/response.rb
# frozen_string_literal: true
# Copyright 2009 Sidu Ponnappa
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at Http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
require 'rexml/document'
module Wrest # :nodoc:
module Native # :nodoc:
# Decorates a response providing support for deserialisation.
#
# The following methods are also available (unlisted by rdoc because they're forwarded to Net::HTTP::Response):
#
# <tt>:@Http_response, :code, :message, :body, :Http_version,
# :[], :content_length, :content_type, :each_header, :each_name, :each_value, :fetch,
# :get_fields, :key?, :type_params</tt>
#
# They behave exactly like their Net::HttpResponse equivalents.
#
# Also provides set of HTTP response code checkers. For instance, the method ok? checks if the response was
# successful with HTTP code 200.
# See HttpCodes for a list of all such response checkers.
class Response
attr_reader :http_response
attr_accessor :deserialised_body
include HttpCodes
extend Forwardable
def_delegators :@http_response, :code, :message, :body, :http_version,
:content_length, :content_type
def_delegators :headers, :[]
# TODO : Are these needed in the Response namespace itself? Can be accessed from the headers method.
def_delegators :@http_response, :each_header, :each_name, :each_value, :fetch,
:get_fields, :key?, :type_params
# We're overriding :new to act as a factory so
# we can build the appropriate Response instance based
# on the response code.
def self.new(http_response)
code = http_response.code.to_i
instance = ((300..303).include?(code) || (305..399).include?(code) ? Wrest::Native::Redirection : self).allocate
instance.send :initialize, http_response
instance
end
def initialize(http_response)
@http_response = http_response
end
def initialize_copy(source)
@headers = source.headers.clone
end
# Checks equality between two Wrest::Native::Response objects.
def ==(other)
return true if equal?(other)
return false unless other.class == self.class
return true if these_fields_are_equal(other)
false
end
# Return the hash of a Wrest::Native::Response object.
def hash
[code, message, headers, http_version, body].hash
end
def deserialise(options = {})
@deserialised_body ||= deserialise_using(Wrest::Components::Translators.lookup(@http_response.content_type),
options)
end
def deserialize(options = {})
deserialise(options)
end
def deserialise_using(translator, options = {})
translator.deserialise(@http_response, options)
end
def deserialize_using(options = {})
deserialise_using(options)
end
# Gives a hash of the response headers. The keys of the hash are case-insensitive.
def headers
return @headers if @headers
nethttp_headers_with_string_values = @http_response.to_hash.transform_values do |old_value|
old_value.is_a?(Array) ? old_value.join(',') : old_value
end
@headers = Wrest::HashWithCaseInsensitiveAccess.new(nethttp_headers_with_string_values)
end
# A null object implementation - invoking this method on
# a response simply returns the same response unless
# the response is Redirection (code 3xx), in which case a
# get is invoked on the url stored in the response headers
# under the key 'location' and the new Response is returned.
def follow(_redirect_request_options = {})
self
end
def connection_closed?
self[Native::StandardHeaders::Connection].downcase == Native::StandardTokens::Close.downcase
end
# Returns whether this response is cacheable.
def cacheable?
cache_configs_set? &&
(!max_age.nil? or (expires_not_in_our_past? && expires_not_in_its_past?)) && pragma_nocache_not_set? &&
vary_header_valid?
end
# :nodoc:
def code_cacheable?
!code.nil? && [200, 203, 300, 301, 302, 304, 307].include?(code.to_i)
end
# :nodoc:
def vary_header_valid?
headers['vary'] != '*'
end
# :nodoc:
def max_age
return @max_age if @max_age
max_age = cache_control_headers.grep(/max-age/)
@max_age = (max_age.first.split('=').last.to_i unless max_age.empty?)
end
def no_cache_flag_not_set?
!cache_control_headers.include?('no-cache')
end
def no_store_flag_not_set?
!cache_control_headers.include?('no-store')
end
def pragma_nocache_not_set?
headers['pragma'].nil? || (!headers['pragma'].include? 'no-cache')
end
# Returns the Date from the response headers.
def response_date
return @response_date if @response_date
@response_date = parse_datefield(headers, 'date')
end
# Returns the Expires date from the response headers.
def expires
return @expires if @expires
@expires = parse_datefield(headers, 'expires')
end
# Returns whether the Expires header of this response is earlier than current time.
def expires_not_in_our_past?
if expires.nil?
false
else
Utils.datetime_to_i(expires) > Time.now.to_i
end
end
# Is the Expires of this response earlier than its Date header.
def expires_not_in_its_past?
# Invalid header value for Date or Expires means the response is not cacheable
if expires.nil? || response_date.nil?
false
else
expires > response_date
end
end
# Age of the response calculated according to RFC 2616 13.2.3
def current_age
current_time = Time.now.to_i
# RFC 2616 13.2.3 Age Calculations. TODO: include response_delay in the calculation as defined in RFC. For this, include original Request with Response.
date_value = begin
Utils.datetime_to_i(DateTime.parse(headers['date']))
rescue StandardError
current_time
end
age_value = headers['age'].to_i || 0
apparent_age = current_time - date_value
[apparent_age, age_value].max
end
# The values in Cache-Control header as an array.
def cache_control_headers
@cache_control_headers ||= recalculate_cache_control_headers
end
# :nodoc:
def recalculate_cache_control_headers
headers['cache-control'].split(',').collect(&:strip)
rescue StandardError
[]
end
# How long (in seconds) is this response expected to be fresh
def freshness_lifetime
@freshness_lifetime ||= recalculate_freshness_lifetime
end
# :nodoc:
def recalculate_freshness_lifetime
return max_age if max_age
response_date = Utils.datetime_to_i(DateTime.parse(headers['date']))
expires_date = Utils.datetime_to_i(DateTime.parse(headers['expires']))
(expires_date - response_date)
end
# Has this response expired? The expiry is calculated from the Max-Age/Expires header.
def expired?
freshness = freshness_lifetime
return true if freshness <= 0
freshness <= current_age
end
def last_modified
headers['last-modified']
end
# Can this response be validated by sending a validation request to the server. The response need to have either
# Last-Modified or ETag header (or both) for it to be validatable.
def can_be_validated?
!(last_modified.nil? and headers['etag'].nil?)
end
# :nodoc:
# helper function. Used to parse date fields.
# this function is used and tested by the expires and response_date methods
def parse_datefield(hash, key)
return unless hash[key]
# Can't trust external input. Do not crash even if invalid dates are passed.
begin
DateTime.parse(hash[key].to_s)
rescue ArgumentError
nil
end
end
private
def cache_configs_set?
code_cacheable? && no_cache_flag_not_set? && no_store_flag_not_set?
end
def these_fields_are_equal(other)
(code == other.code) &&
(headers == other.headers) &&
(http_version == other.http_version) &&
(message == other.message) &&
(body == other.body)
end
end
end
end