Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

460 lines (399 sloc) 14.098 kB
require 'base64'
require 'delegate'
require 'time'
module VCR
# @private
module Normalizers
# @private
module Body
def self.included(klass)
klass.extend ClassMethods
end
# @private
module ClassMethods
def body_from(hash_or_string)
return hash_or_string unless hash_or_string.is_a?(Hash)
hash = hash_or_string
if hash.has_key?('base64_string')
string = Base64.decode64(hash['base64_string'])
force_encode_string(string, hash['encoding'])
else
try_encode_string(hash['string'], hash['encoding'])
end
end
if "".respond_to?(:encoding)
def force_encode_string(string, encoding)
return string unless encoding
string.force_encoding(encoding)
end
def try_encode_string(string, encoding)
return string if encoding.nil? || string.encoding.name == encoding
# ASCII-8BIT just means binary, so encoding to it is nonsensical
# and yet "\u00f6".encode("ASCII-8BIT") raises an error.
# Instead, we'll force encode it (essentially just tagging it as binary)
return string.force_encoding(encoding) if encoding == "ASCII-8BIT"
string.encode(encoding)
rescue EncodingError => e
struct_type = name.split('::').last.downcase
warn "VCR: got `#{e.class.name}: #{e.message}` while trying to encode the #{string.encoding.name} " +
"#{struct_type} body to the original body encoding (#{encoding}). Consider using the " +
"`:preserve_exact_body_bytes` option to work around this."
return string
end
else
def force_encode_string(string, encoding)
string
end
def try_encode_string(string, encoding)
string
end
end
end
def initialize(*args)
super
# Ensure that the body is a raw string, in case the string instance
# has been subclassed or extended with additional instance variables
# or attributes, so that it is serialized to YAML as a raw string.
# This is needed for rest-client. See this ticket for more info:
# http://github.com/myronmarston/vcr/issues/4
self.body = String.new(body.to_s)
end
private
def serializable_body
if VCR.configuration.preserve_exact_body_bytes_for?(self)
base_body_hash(body).merge('base64_string' => Base64.encode64(body))
else
base_body_hash(body).merge('string' => body)
end
end
if ''.respond_to?(:encoding)
def base_body_hash(body)
{ 'encoding' => body.encoding.name }
end
else
def base_body_hash(body)
{ }
end
end
end
# @private
module Header
def initialize(*args)
super
normalize_headers
end
private
def normalize_headers
new_headers = {}
headers.each do |k, v|
val_array = case v
when Array then v
when nil then []
else [v]
end
new_headers[k] = convert_to_raw_strings(val_array)
end if headers
self.headers = new_headers
end
def convert_to_raw_strings(array)
# Ensure the values are raw strings.
# Apparently for Paperclip uploads to S3, headers
# get serialized with some extra stuff which leads
# to a seg fault. See this issue for more info:
# https://github.com/myronmarston/vcr/issues#issue/39
array.map do |v|
case v
when String; String.new(v)
when Array; convert_to_raw_strings(v)
else v
end
end
end
end
end
# @private
module OrderedHashSerializer
def each
@ordered_keys.each do |key|
yield key, self[key]
end
end
if RUBY_VERSION =~ /1.9/
# 1.9 hashes are already ordered.
def self.apply_to(*args); end
else
def self.apply_to(hash, keys)
hash.instance_variable_set(:@ordered_keys, keys)
hash.extend self
end
end
end
# The request of an {HTTPInteraction}.
#
# @attr [Symbol] method the HTTP method (i.e. :head, :options, :get, :post, :put, :patch or :delete)
# @attr [String] uri the request URI
# @attr [String, nil] body the request body
# @attr [Hash{String => Array<String>}] headers the request headers
class Request < Struct.new(:method, :uri, :body, :headers)
include Normalizers::Header
include Normalizers::Body
def initialize(*args)
skip_port_stripping = false
if args.last == :skip_port_stripping
skip_port_stripping = true
args.pop
end
super(*args)
self.method = self.method.to_s.downcase.to_sym if self.method
self.uri = without_standard_port(self.uri) unless skip_port_stripping
end
# Builds a serializable hash from the request data.
#
# @return [Hash] hash that represents this request and can be easily
# serialized.
# @see Request.from_hash
def to_hash
{
'method' => method.to_s,
'uri' => uri,
'body' => serializable_body,
'headers' => headers
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
# Constructs a new instance from a hash.
#
# @param [Hash] hash the hash to use to construct the instance.
# @return [Request] the request
def self.from_hash(hash)
method = hash['method']
method &&= method.to_sym
new method,
hash['uri'],
body_from(hash['body']),
hash['headers'],
:skip_port_stripping
end
@@object_method = Object.instance_method(:method)
def method(*args)
return super if args.empty?
@@object_method.bind(self).call(*args)
end
# Decorates a {Request} with its current type.
class Typed < DelegateClass(self)
# @return [Symbol] One of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
attr_reader :type
# @param [Request] request the request
# @param [Symbol] type the type. Should be one of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
def initialize(request, type)
@type = type
super(request)
end
# @return [Boolean] whether or not this request is being ignored
def ignored?
type == :ignored
end
# @return [Boolean] whether or not this request will be stubbed
def stubbed?
type == :stubbed
end
# @return [Boolean] whether or not this request will be recorded.
def recordable?
type == :recordable
end
# @return [Boolean] whether or not VCR knows how to handle this request.
def unhandled?
type == :unhandled
end
# @return [Boolean] whether or not this request will be made for real.
# @note VCR allows `:ignored` and `:recordable` requests to be made for real.
def real?
ignored? || recordable?
end
undef method
end
# Provides fiber-awareness for the {VCR::Configuration#around_http_request} hook.
class FiberAware < DelegateClass(Typed)
# Yields the fiber so the request can proceed.
#
# @return [VCR::Response] the response from the request
def proceed
Fiber.yield
end
# Builds a proc that allows the request to proceed when called.
# This allows you to treat the request as a proc and pass it on
# to a method that yields (at which point the request will proceed).
#
# @return [Proc] the proc
def to_proc
lambda { proceed }
end
undef method
end
# Transforms the request into a fiber aware one by extending
# the {FiberAware} module onto the instance. Necessary for the
# {VCR::Configuration#around_http_request} hook.
#
# @return [Request] the request instance
def fiber_aware
extend FiberAware
end
private
def without_standard_port(uri)
return uri if uri.nil?
u = URI(uri)
return uri unless [['http', 80], ['https', 443]].include?([u.scheme, u.port])
u.port = nil
u.to_s
end
end
# The response of an {HTTPInteraction}.
#
# @attr [ResponseStatus] status the status of the response
# @attr [Hash{String => Array<String>}] headers the response headers
# @attr [String] body the response body
# @attr [nil, String] http_version the HTTP version
class Response < Struct.new(:status, :headers, :body, :http_version)
include Normalizers::Header
include Normalizers::Body
# Builds a serializable hash from the response data.
#
# @return [Hash] hash that represents this response
# and can be easily serialized.
# @see Response.from_hash
def to_hash
{
'status' => status.to_hash,
'headers' => headers,
'body' => serializable_body,
'http_version' => http_version
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
# Constructs a new instance from a hash.
#
# @param [Hash] hash the hash to use to construct the instance.
# @return [Response] the response
def self.from_hash(hash)
new ResponseStatus.from_hash(hash.fetch('status', {})),
hash['headers'],
body_from(hash['body']),
hash['http_version']
end
# Updates the Content-Length response header so that it is
# accurate for the response body.
def update_content_length_header
value = body ? body.bytesize.to_s : '0'
key = %w[ Content-Length content-length ].find { |k| headers.has_key?(k) }
headers[key] = [value] if key
end
end
# The response status of an {HTTPInteraction}.
#
# @attr [Integer] code the HTTP status code
# @attr [String] message the HTTP status message (e.g. "OK" for a status of 200)
class ResponseStatus < Struct.new(:code, :message)
# Builds a serializable hash from the response status data.
#
# @return [Hash] hash that represents this response status
# and can be easily serialized.
# @see ResponseStatus.from_hash
def to_hash
{
'code' => code, 'message' => message
}.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
# Constructs a new instance from a hash.
#
# @param [Hash] hash the hash to use to construct the instance.
# @return [ResponseStatus] the response status
def self.from_hash(hash)
new hash['code'], hash['message']
end
end
# Represents a single interaction over HTTP, containing a request and a response.
#
# @attr [Request] request the request
# @attr [Response] response the response
# @attr [Time] recorded_at when this HTTP interaction was recorded
class HTTPInteraction < Struct.new(:request, :response, :recorded_at)
def initialize(*args)
super
self.recorded_at ||= Time.now
end
# Builds a serializable hash from the HTTP interaction data.
#
# @return [Hash] hash that represents this HTTP interaction
# and can be easily serialized.
# @see HTTPInteraction.from_hash
def to_hash
{
'request' => request.to_hash,
'response' => response.to_hash,
'recorded_at' => recorded_at.httpdate
}.tap do |hash|
OrderedHashSerializer.apply_to(hash, members)
end
end
# Constructs a new instance from a hash.
#
# @param [Hash] hash the hash to use to construct the instance.
# @return [HTTPInteraction] the HTTP interaction
def self.from_hash(hash)
new Request.from_hash(hash.fetch('request', {})),
Response.from_hash(hash.fetch('response', {})),
Time.httpdate(hash.fetch('recorded_at'))
end
# @return [HookAware] an instance with additional capabilities
# suitable for use in `before_record` and `before_playback` hooks.
def hook_aware
HookAware.new(self)
end
# Decorates an {HTTPInteraction} with additional methods useful
# for a `before_record` or `before_playback` hook.
class HookAware < DelegateClass(HTTPInteraction)
def initialize(http_interaction)
@ignored = false
super
end
# Flags the HTTP interaction so that VCR ignores it. This is useful in
# a {VCR::Configuration#before_record} or {VCR::Configuration#before_playback}
# hook so that VCR does not record or play it back.
# @see #ignored?
def ignore!
@ignored = true
end
# @return [Boolean] whether or not this HTTP interaction should be ignored.
# @see #ignore!
def ignored?
!!@ignored
end
# Replaces a string in any part of the HTTP interaction (headers, request body,
# response body, etc) with the given replacement text.
#
# @param [String] text the text to replace
# @param [String] replacement_text the text to put in its place
def filter!(text, replacement_text)
return self if [text, replacement_text].any? { |t| t.to_s.empty? }
filter_object!(self, text, replacement_text)
end
private
def filter_object!(object, text, replacement_text)
if object.respond_to?(:gsub)
object.gsub!(text, replacement_text) if object.include?(text)
elsif Hash === object
filter_hash!(object, text, replacement_text)
elsif object.respond_to?(:each)
# This handles nested arrays and structs
object.each { |o| filter_object!(o, text, replacement_text) }
end
object
end
def filter_hash!(hash, text, replacement_text)
filter_object!(hash.values, text, replacement_text)
hash.keys.each do |k|
new_key = filter_object!(k.dup, text, replacement_text)
hash[new_key] = hash.delete(k) unless k == new_key
end
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.