lib/vcr/structs.rb in vcr-2.0.0.rc1 vs lib/vcr/structs.rb in vcr-2.0.0.rc2
- old
+ new
@@ -1,22 +1,91 @@
+require 'base64'
+require 'delegate'
require 'time'
-require 'forwardable'
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 string.encoding.name == encoding
+ 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
@@ -54,10 +123,11 @@
end
end
end
end
+ # @private
module OrderedHashSerializer
def each
@ordered_keys.each do |key|
yield key, self[key]
end
@@ -72,59 +142,130 @@
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)
super
self.method = self.method.to_s.downcase.to_sym if self.method
self.uri = without_standard_port(self.uri)
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' => body,
+ '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'],
- hash['body'],
+ body_from(hash['body']),
hash['headers']
end
@@object_method = Object.instance_method(:method)
def method(*args)
return super if args.empty?
@@object_method.bind(self).call(*args)
end
- # transforms the request into a fiber aware one
- def fiber_aware
- extend FiberAware
+ # 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
- module FiberAware
+ # 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)
@@ -132,107 +273,173 @@
u.port = nil
u.to_s
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)
- extend ::Forwardable
- def_delegators :request, :uri, :method
-
def initialize(*args)
- @ignored = false
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
- def ignore!
- @ignored = true
+ # @return [HookAware] an instance with additional capabilities
+ # suitable for use in `before_record` and `before_playback` hooks.
+ def hook_aware
+ HookAware.new(self)
end
- def ignored?
- !!@ignored
- 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
- def filter!(text, replacement_text)
- return self if [text, replacement_text].any? { |t| t.to_s.empty? }
- filter_object!(self, text, replacement_text)
- 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
- private
+ # @return [Boolean] whether or not this HTTP interaction should be ignored.
+ # @see #ignore!
+ def ignored?
+ !!@ignored
+ end
- 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) }
+ # 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
- object
- end
+ private
- def filter_hash!(hash, text, replacement_text)
- filter_object!(hash.values, text, replacement_text)
+ 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
- hash.keys.each do |k|
- new_key = filter_object!(k.dup, text, replacement_text)
- hash[new_key] = hash.delete(k) unless k == new_key
+ 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
+ # 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' => body,
+ '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'],
- hash['body'],
+ 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
end