vcr 2.0.0.rc1 → 2.0.0.rc2

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 (113) hide show
  1. data/.gitignore +2 -0
  2. data/.limited_red +1 -0
  3. data/.travis.yml +10 -1
  4. data/.yardopts +9 -0
  5. data/CHANGELOG.md +51 -1
  6. data/Gemfile +5 -1
  7. data/LICENSE +1 -1
  8. data/README.md +23 -28
  9. data/Rakefile +63 -18
  10. data/Upgrade.md +200 -0
  11. data/features/.nav +2 -0
  12. data/features/cassettes/automatic_re_recording.feature +19 -15
  13. data/features/cassettes/dynamic_erb.feature +12 -4
  14. data/features/cassettes/exclusive.feature +31 -23
  15. data/features/cassettes/format.feature +54 -30
  16. data/features/cassettes/naming.feature +1 -1
  17. data/features/cassettes/update_content_length_header.feature +16 -12
  18. data/features/configuration/allow_http_connections_when_no_cassette.feature +1 -1
  19. data/features/configuration/debug_logging.feature +52 -0
  20. data/features/configuration/filter_sensitive_data.feature +4 -4
  21. data/features/configuration/hook_into.feature +5 -2
  22. data/features/configuration/ignore_request.feature +5 -3
  23. data/features/configuration/preserve_exact_body_bytes.feature +103 -0
  24. data/features/hooks/after_http_request.feature +17 -4
  25. data/features/hooks/around_http_request.feature +2 -1
  26. data/features/hooks/before_http_request.feature +25 -8
  27. data/features/hooks/before_playback.feature +16 -12
  28. data/features/hooks/before_record.feature +2 -2
  29. data/features/http_libraries/em_http_request.feature +82 -58
  30. data/features/http_libraries/net_http.feature +6 -6
  31. data/features/middleware/faraday.feature +2 -1
  32. data/features/middleware/rack.feature +2 -2
  33. data/features/record_modes/all.feature +19 -15
  34. data/features/record_modes/new_episodes.feature +17 -13
  35. data/features/record_modes/none.feature +15 -11
  36. data/features/record_modes/once.feature +16 -12
  37. data/features/request_matching/body.feature +28 -20
  38. data/features/request_matching/custom_matcher.feature +28 -20
  39. data/features/request_matching/headers.feature +34 -26
  40. data/features/request_matching/host.feature +28 -20
  41. data/features/request_matching/identical_request_sequence.feature +28 -20
  42. data/features/request_matching/method.feature +28 -20
  43. data/features/request_matching/path.feature +28 -20
  44. data/features/request_matching/playback_repeats.feature +28 -20
  45. data/features/request_matching/uri.feature +28 -20
  46. data/features/request_matching/uri_without_param.feature +28 -20
  47. data/features/support/env.rb +7 -6
  48. data/features/support/vcr_cucumber_helpers.rb +1 -0
  49. data/features/test_frameworks/cucumber.feature +8 -8
  50. data/features/test_frameworks/rspec_macro.feature +4 -4
  51. data/features/test_frameworks/rspec_metadata.feature +6 -6
  52. data/features/test_frameworks/shoulda.feature +1 -1
  53. data/features/test_frameworks/test_unit.feature +1 -1
  54. data/lib/vcr.rb +156 -5
  55. data/lib/vcr/cassette.rb +80 -30
  56. data/lib/vcr/cassette/http_interaction_list.rb +33 -4
  57. data/lib/vcr/cassette/migrator.rb +2 -3
  58. data/lib/vcr/cassette/reader.rb +1 -0
  59. data/lib/vcr/cassette/serializers.rb +22 -0
  60. data/lib/vcr/cassette/serializers/json.rb +27 -2
  61. data/lib/vcr/cassette/serializers/psych.rb +26 -2
  62. data/lib/vcr/cassette/serializers/syck.rb +28 -2
  63. data/lib/vcr/cassette/serializers/yaml.rb +28 -2
  64. data/lib/vcr/configuration.rb +348 -10
  65. data/lib/vcr/deprecations.rb +8 -0
  66. data/lib/vcr/errors.rb +40 -0
  67. data/lib/vcr/extensions/net_http_response.rb +12 -11
  68. data/lib/vcr/library_hooks.rb +1 -0
  69. data/lib/vcr/library_hooks/excon.rb +24 -3
  70. data/lib/vcr/library_hooks/fakeweb.rb +32 -16
  71. data/lib/vcr/library_hooks/faraday.rb +3 -0
  72. data/lib/vcr/library_hooks/typhoeus.rb +40 -37
  73. data/lib/vcr/library_hooks/webmock.rb +54 -34
  74. data/lib/vcr/middleware/faraday.rb +13 -0
  75. data/lib/vcr/middleware/rack.rb +35 -0
  76. data/lib/vcr/request_handler.rb +60 -8
  77. data/lib/vcr/request_ignorer.rb +1 -0
  78. data/lib/vcr/request_matcher_registry.rb +28 -0
  79. data/lib/vcr/structs.rb +245 -38
  80. data/lib/vcr/test_frameworks/cucumber.rb +10 -0
  81. data/lib/vcr/test_frameworks/rspec.rb +26 -1
  82. data/lib/vcr/util/hooks.rb +29 -27
  83. data/lib/vcr/util/internet_connection.rb +2 -0
  84. data/lib/vcr/util/logger.rb +25 -0
  85. data/lib/vcr/util/variable_args_block_caller.rb +1 -0
  86. data/lib/vcr/util/version_checker.rb +1 -0
  87. data/lib/vcr/version.rb +8 -1
  88. data/spec/capture_warnings.rb +3 -3
  89. data/spec/monkey_patches.rb +28 -13
  90. data/spec/spec_helper.rb +17 -0
  91. data/spec/support/http_library_adapters.rb +7 -4
  92. data/spec/support/shared_example_groups/hook_into_http_library.rb +96 -32
  93. data/spec/support/shared_example_groups/request_hooks.rb +9 -8
  94. data/spec/support/sinatra_app.rb +3 -1
  95. data/spec/support/vcr_localhost_server.rb +1 -0
  96. data/spec/vcr/cassette/http_interaction_list_spec.rb +119 -54
  97. data/spec/vcr/cassette/migrator_spec.rb +19 -6
  98. data/spec/vcr/cassette/serializers_spec.rb +51 -6
  99. data/spec/vcr/cassette_spec.rb +44 -19
  100. data/spec/vcr/configuration_spec.rb +91 -6
  101. data/spec/vcr/library_hooks/excon_spec.rb +54 -16
  102. data/spec/vcr/library_hooks/fakeweb_spec.rb +12 -21
  103. data/spec/vcr/library_hooks/typhoeus_spec.rb +2 -29
  104. data/spec/vcr/library_hooks/webmock_spec.rb +4 -18
  105. data/spec/vcr/middleware/faraday_spec.rb +1 -16
  106. data/spec/vcr/structs_spec.rb +194 -61
  107. data/spec/vcr/test_frameworks/rspec_spec.rb +10 -0
  108. data/spec/vcr/util/hooks_spec.rb +104 -56
  109. data/spec/vcr/util/version_checker_spec.rb +45 -0
  110. data/spec/vcr_spec.rb +11 -0
  111. data/vcr.gemspec +30 -34
  112. metadata +149 -95
  113. data/spec/support/shared_example_groups/version_checking.rb +0 -34
@@ -5,19 +5,32 @@ require 'vcr/request_handler'
5
5
  VCR::VersionChecker.new('Faraday', Faraday::VERSION, '0.7.0', '0.7').check_version!
6
6
 
7
7
  module VCR
8
+ # Contains middlewares for use with different libraries.
8
9
  module Middleware
10
+ # Faraday middleware that VCR uses to record and replay HTTP requests made through
11
+ # Faraday.
12
+ #
13
+ # @note You can either insert this middleware into the Faraday middleware stack
14
+ # yourself or configure {VCR::Configuration#hook_into} to hook into +:faraday+.
9
15
  class Faraday
10
16
  include VCR::Deprecations::Middleware::Faraday
11
17
 
18
+ # Constructs a new instance of the Faraday middleware.
19
+ #
20
+ # @param [#call] the faraday app
12
21
  def initialize(app)
13
22
  super
14
23
  @app = app
15
24
  end
16
25
 
26
+ # Handles the HTTP request being made through Faraday
27
+ #
28
+ # @param [Hash] env the Faraday request env hash
17
29
  def call(env)
18
30
  RequestHandler.new(@app, env).handle
19
31
  end
20
32
 
33
+ # @private
21
34
  class RequestHandler < ::VCR::RequestHandler
22
35
  attr_reader :app, :env
23
36
  def initialize(app, env)
@@ -1,29 +1,64 @@
1
1
  module VCR
2
2
  module Middleware
3
+ # Object yielded by VCR's {Rack} middleware that allows you to configure
4
+ # the cassette dynamically based on the rack env.
3
5
  class CassetteArguments
6
+ # @private
4
7
  def initialize
5
8
  @name = nil
6
9
  @options = {}
7
10
  end
8
11
 
12
+ # Sets (and gets) the cassette name.
13
+ #
14
+ # @param [#to_s] name the cassette name
15
+ # @return [#to_s] the cassette name
9
16
  def name(name = nil)
10
17
  @name = name if name
11
18
  @name
12
19
  end
13
20
 
21
+ # Sets (and gets) the cassette options.
22
+ #
23
+ # @param [Hash] options the cassette options
24
+ # @return [Hash] the cassette options
14
25
  def options(options = {})
15
26
  @options.merge!(options)
16
27
  end
17
28
  end
18
29
 
30
+ # Rack middleware that uses a VCR cassette for each incoming HTTP request.
31
+ #
32
+ # @example
33
+ # app = Rack::Builder.new do
34
+ # use VCR::Middleware::Rack do |cassette, env|
35
+ # cassette.name "rack/#{env['SERVER_NAME']}"
36
+ # cassette.options :record => :new_episodes
37
+ # end
38
+ #
39
+ # run MyRackApp
40
+ # end
41
+ #
42
+ # @note This will record/replay _outbound_ HTTP requests made by your rack app.
19
43
  class Rack
20
44
  include VCR::VariableArgsBlockCaller
21
45
 
46
+ # Constructs a new instance of VCR's rack middleware.
47
+ #
48
+ # @param [#call] app the rack app
49
+ # @yield the cassette configuration block
50
+ # @yieldparam [CassetteArguments] cassette the cassette configuration object
51
+ # @yieldparam [(optional) Hash] env the rack env hash
52
+ # @raise [ArgumentError] if no configuration block is provided
22
53
  def initialize(app, &block)
23
54
  raise ArgumentError.new("You must provide a block to set the cassette options") unless block
24
55
  @app, @cassette_arguments_block, @mutex = app, block, Mutex.new
25
56
  end
26
57
 
58
+ # Implements the rack middleware interface.
59
+ #
60
+ # @param [Hash] env the rack env hash
61
+ # @return [Array(Integer, Hash, #each)] the rack response
27
62
  def call(env)
28
63
  @mutex.synchronize do
29
64
  VCR.use_cassette(*cassette_arguments(env)) do
@@ -1,23 +1,53 @@
1
1
  module VCR
2
+ # @private
2
3
  class RequestHandler
4
+ include Logger
5
+
3
6
  def handle
7
+ log "Handling request: #{request_summary} (disabled: #{disabled?})"
4
8
  invoke_before_request_hook
5
- return on_ignored_request if should_ignore?
6
- return on_stubbed_request if stubbed_response
7
- return on_recordable_request if VCR.real_http_connections_allowed?
8
- on_connection_not_allowed
9
+
10
+ req_type = request_type(:consume_stub)
11
+
12
+ log "Identified request type (#{req_type}) for #{request_summary}"
13
+
14
+ # The before_request hook can change the type of request
15
+ # (i.e. by inserting a cassette), so we need to query the
16
+ # request type again.
17
+ #
18
+ # Likewise, the main handler logic an modify what
19
+ # #request_type would return (i.e. when a response stub is
20
+ # used), so we need to store the request type for the
21
+ # the after_request hook.
22
+ set_typed_request_for_after_hook(req_type)
23
+
24
+ send "on_#{req_type}_request"
9
25
  end
10
26
 
11
27
  private
12
28
 
29
+ def set_typed_request_for_after_hook(request_type)
30
+ @after_hook_typed_request = Request::Typed.new(vcr_request, request_type)
31
+ end
32
+
33
+ def request_type(consume_stub = false)
34
+ case
35
+ when should_ignore? then :ignored
36
+ when has_response_stub?(consume_stub) then :stubbed
37
+ when VCR.real_http_connections_allowed? then :recordable
38
+ else :unhandled
39
+ end
40
+ end
41
+
13
42
  def invoke_before_request_hook
14
- return if disabled?
15
- VCR.configuration.invoke_hook(:before_http_request, vcr_request)
43
+ return if disabled? || !VCR.configuration.has_hooks_for?(:before_http_request)
44
+ typed_request = Request::Typed.new(vcr_request, request_type)
45
+ VCR.configuration.invoke_hook(:before_http_request, typed_request)
16
46
  end
17
47
 
18
48
  def invoke_after_request_hook(vcr_response)
19
49
  return if disabled?
20
- VCR.configuration.invoke_hook(:after_http_request, vcr_request, vcr_response)
50
+ VCR.configuration.invoke_hook(:after_http_request, @after_hook_typed_request, vcr_response)
21
51
  end
22
52
 
23
53
  def should_ignore?
@@ -28,6 +58,14 @@ module VCR
28
58
  VCR.library_hooks.disabled?(library_name)
29
59
  end
30
60
 
61
+ def has_response_stub?(consume_stub)
62
+ if consume_stub
63
+ stubbed_response
64
+ else
65
+ VCR.http_interactions.has_interaction_matching?(vcr_request)
66
+ end
67
+ end
68
+
31
69
  def stubbed_response
32
70
  @stubbed_response ||= VCR.http_interactions.response_for(vcr_request)
33
71
  end
@@ -47,8 +85,22 @@ module VCR
47
85
  def on_recordable_request
48
86
  end
49
87
 
50
- def on_connection_not_allowed
88
+ def on_unhandled_request
51
89
  raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request)
52
90
  end
91
+
92
+ def request_summary
93
+ request_matchers = if cass = VCR.current_cassette
94
+ cass.match_requests_on
95
+ else
96
+ VCR.configuration.default_cassette_options[:match_requests_on]
97
+ end
98
+
99
+ super(vcr_request, request_matchers)
100
+ end
101
+
102
+ def log_prefix
103
+ "[#{library_name}] "
104
+ end
53
105
  end
54
106
  end
@@ -3,6 +3,7 @@ require 'set'
3
3
  require 'vcr/util/hooks'
4
4
 
5
5
  module VCR
6
+ # @private
6
7
  class RequestIgnorer
7
8
  include VCR::Hooks
8
9
 
@@ -1,15 +1,21 @@
1
1
  require 'vcr/errors'
2
2
 
3
3
  module VCR
4
+ # Keeps track of the different request matchers.
4
5
  class RequestMatcherRegistry
6
+
7
+ # The default request matchers used for any cassette that does not
8
+ # specify request matchers.
5
9
  DEFAULT_MATCHERS = [:method, :uri]
6
10
 
11
+ # @private
7
12
  class Matcher < Struct.new(:callable)
8
13
  def matches?(request_1, request_2)
9
14
  callable.call(request_1, request_2)
10
15
  end
11
16
  end
12
17
 
18
+ # @private
13
19
  class URIWithoutParamsMatcher < Struct.new(:params_to_ignore)
14
20
  def partial_uri_from(request)
15
21
  URI(request.uri).tap do |uri|
@@ -37,11 +43,13 @@ module VCR
37
43
  end
38
44
  end
39
45
 
46
+ # @private
40
47
  def initialize
41
48
  @registry = {}
42
49
  register_built_ins
43
50
  end
44
51
 
52
+ # @private
45
53
  def register(name, &block)
46
54
  if @registry.has_key?(name)
47
55
  warn "WARNING: There is already a VCR request matcher registered for #{name.inspect}. Overriding it."
@@ -50,6 +58,7 @@ module VCR
50
58
  @registry[name] = Matcher.new(block)
51
59
  end
52
60
 
61
+ # @private
53
62
  def [](matcher)
54
63
  @registry.fetch(matcher) do
55
64
  matcher.respond_to?(:call) ?
@@ -58,6 +67,25 @@ module VCR
58
67
  end
59
68
  end
60
69
 
70
+ # Builds a dynamic request matcher that matches on a URI while ignoring the
71
+ # named query parameters. This is useful for dealing with non-deterministic
72
+ # URIs (i.e. that have a timestamp or request signature parameter).
73
+ #
74
+ # @example
75
+ # without_timestamp = VCR.request_matchers.uri_without_param(:timestamp)
76
+ #
77
+ # # use it directly...
78
+ # VCR.use_cassette('example', :match_requests_on => [:method, without_timestamp]) { }
79
+ #
80
+ # # ...or register it as a named matcher
81
+ # VCR.configure do |c|
82
+ # c.register_request_matcher(:uri_without_timestamp, &without_timestamp)
83
+ # end
84
+ #
85
+ # VCR.use_cassette('example', :match_requests_on => [:method, :uri_without_timestamp]) { }
86
+ #
87
+ # @param ignores [Array<#to_s>] The names of the query parameters to ignore
88
+ # @return [#call] the request matcher
61
89
  def uri_without_params(*ignores)
62
90
  uri_without_param_matchers[ignores]
63
91
  end
data/lib/vcr/structs.rb CHANGED
@@ -1,9 +1,57 @@
1
+ require 'base64'
2
+ require 'delegate'
1
3
  require 'time'
2
- require 'forwardable'
3
4
 
4
5
  module VCR
6
+ # @private
5
7
  module Normalizers
8
+ # @private
6
9
  module Body
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ end
13
+
14
+ # @private
15
+ module ClassMethods
16
+ def body_from(hash_or_string)
17
+ return hash_or_string unless hash_or_string.is_a?(Hash)
18
+ hash = hash_or_string
19
+
20
+ if hash.has_key?('base64_string')
21
+ string = Base64.decode64(hash['base64_string'])
22
+ force_encode_string(string, hash['encoding'])
23
+ else
24
+ try_encode_string(hash['string'], hash['encoding'])
25
+ end
26
+ end
27
+
28
+ if "".respond_to?(:encoding)
29
+ def force_encode_string(string, encoding)
30
+ return string unless encoding
31
+ string.force_encoding(encoding)
32
+ end
33
+
34
+ def try_encode_string(string, encoding)
35
+ return string if string.encoding.name == encoding
36
+ string.encode(encoding)
37
+ rescue EncodingError => e
38
+ struct_type = name.split('::').last.downcase
39
+ warn "VCR: got `#{e.class.name}: #{e.message}` while trying to encode the #{string.encoding.name} " +
40
+ "#{struct_type} body to the original body encoding (#{encoding}). Consider using the " +
41
+ "`:preserve_exact_body_bytes` option to work around this."
42
+ return string
43
+ end
44
+ else
45
+ def force_encode_string(string, encoding)
46
+ string
47
+ end
48
+
49
+ def try_encode_string(string, encoding)
50
+ string
51
+ end
52
+ end
53
+ end
54
+
7
55
  def initialize(*args)
8
56
  super
9
57
  # Ensure that the body is a raw string, in case the string instance
@@ -13,8 +61,29 @@ module VCR
13
61
  # http://github.com/myronmarston/vcr/issues/4
14
62
  self.body = String.new(body.to_s)
15
63
  end
64
+
65
+ private
66
+
67
+ def serializable_body
68
+ if VCR.configuration.preserve_exact_body_bytes_for?(self)
69
+ base_body_hash(body).merge('base64_string' => Base64.encode64(body))
70
+ else
71
+ base_body_hash(body).merge('string' => body)
72
+ end
73
+ end
74
+
75
+ if ''.respond_to?(:encoding)
76
+ def base_body_hash(body)
77
+ { 'encoding' => body.encoding.name }
78
+ end
79
+ else
80
+ def base_body_hash(body)
81
+ { }
82
+ end
83
+ end
16
84
  end
17
85
 
86
+ # @private
18
87
  module Header
19
88
  def initialize(*args)
20
89
  super
@@ -56,6 +125,7 @@ module VCR
56
125
  end
57
126
  end
58
127
 
128
+ # @private
59
129
  module OrderedHashSerializer
60
130
  def each
61
131
  @ordered_keys.each do |key|
@@ -74,6 +144,12 @@ module VCR
74
144
  end
75
145
  end
76
146
 
147
+ # The request of an {HTTPInteraction}.
148
+ #
149
+ # @attr [Symbol] method the HTTP method (i.e. :head, :options, :get, :post, :put, :patch or :delete)
150
+ # @attr [String] uri the request URI
151
+ # @attr [String, nil] body the request body
152
+ # @attr [Hash{String => Array<String>}] headers the request headers
77
153
  class Request < Struct.new(:method, :uri, :body, :headers)
78
154
  include Normalizers::Header
79
155
  include Normalizers::Body
@@ -84,21 +160,30 @@ module VCR
84
160
  self.uri = without_standard_port(self.uri)
85
161
  end
86
162
 
163
+ # Builds a serializable hash from the request data.
164
+ #
165
+ # @return [Hash] hash that represents this request and can be easily
166
+ # serialized.
167
+ # @see Request.from_hash
87
168
  def to_hash
88
169
  {
89
170
  'method' => method.to_s,
90
171
  'uri' => uri,
91
- 'body' => body,
172
+ 'body' => serializable_body,
92
173
  'headers' => headers
93
174
  }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
94
175
  end
95
176
 
177
+ # Constructs a new instance from a hash.
178
+ #
179
+ # @param [Hash] hash the hash to use to construct the instance.
180
+ # @return [Request] the request
96
181
  def self.from_hash(hash)
97
182
  method = hash['method']
98
183
  method &&= method.to_sym
99
184
  new method,
100
185
  hash['uri'],
101
- hash['body'],
186
+ body_from(hash['body']),
102
187
  hash['headers']
103
188
  end
104
189
 
@@ -108,19 +193,75 @@ module VCR
108
193
  @@object_method.bind(self).call(*args)
109
194
  end
110
195
 
111
- # transforms the request into a fiber aware one
112
- def fiber_aware
113
- extend FiberAware
196
+ # Decorates a {Request} with its current type.
197
+ class Typed < DelegateClass(self)
198
+ # @return [Symbol] One of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
199
+ attr_reader :type
200
+
201
+ # @param [Request] request the request
202
+ # @param [Symbol] type the type. Should be one of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`.
203
+ def initialize(request, type)
204
+ @type = type
205
+ super(request)
206
+ end
207
+
208
+ # @return [Boolean] whether or not this request is being ignored
209
+ def ignored?
210
+ type == :ignored
211
+ end
212
+
213
+ # @return [Boolean] whether or not this request will be stubbed
214
+ def stubbed?
215
+ type == :stubbed
216
+ end
217
+
218
+ # @return [Boolean] whether or not this request will be recorded.
219
+ def recordable?
220
+ type == :recordable
221
+ end
222
+
223
+ # @return [Boolean] whether or not VCR knows how to handle this request.
224
+ def unhandled?
225
+ type == :unhandled
226
+ end
227
+
228
+ # @return [Boolean] whether or not this request will be made for real.
229
+ # @note VCR allows `:ignored` and `:recordable` requests to be made for real.
230
+ def real?
231
+ ignored? || recordable?
232
+ end
233
+
234
+ undef method
114
235
  end
115
236
 
116
- module FiberAware
237
+ # Provides fiber-awareness for the {VCR::Configuration#around_http_request} hook.
238
+ class FiberAware < DelegateClass(Typed)
239
+ # Yields the fiber so the request can proceed.
240
+ #
241
+ # @return [VCR::Response] the response from the request
117
242
  def proceed
118
243
  Fiber.yield
119
244
  end
120
245
 
246
+ # Builds a proc that allows the request to proceed when called.
247
+ # This allows you to treat the request as a proc and pass it on
248
+ # to a method that yields (at which point the request will proceed).
249
+ #
250
+ # @return [Proc] the proc
121
251
  def to_proc
122
252
  lambda { proceed }
123
253
  end
254
+
255
+ undef method
256
+ end
257
+
258
+ # Transforms the request into a fiber aware one by extending
259
+ # the {FiberAware} module onto the instance. Necessary for the
260
+ # {VCR::Configuration#around_http_request} hook.
261
+ #
262
+ # @return [Request] the request instance
263
+ def fiber_aware
264
+ extend FiberAware
124
265
  end
125
266
 
126
267
  private
@@ -134,16 +275,22 @@ module VCR
134
275
  end
135
276
  end
136
277
 
278
+ # Represents a single interaction over HTTP, containing a request and a response.
279
+ #
280
+ # @attr [Request] request the request
281
+ # @attr [Response] response the response
282
+ # @attr [Time] recorded_at when this HTTP interaction was recorded
137
283
  class HTTPInteraction < Struct.new(:request, :response, :recorded_at)
138
- extend ::Forwardable
139
- def_delegators :request, :uri, :method
140
-
141
284
  def initialize(*args)
142
- @ignored = false
143
285
  super
144
286
  self.recorded_at ||= Time.now
145
287
  end
146
288
 
289
+ # Builds a serializable hash from the HTTP interaction data.
290
+ #
291
+ # @return [Hash] hash that represents this HTTP interaction
292
+ # and can be easily serialized.
293
+ # @see HTTPInteraction.from_hash
147
294
  def to_hash
148
295
  {
149
296
  'request' => request.to_hash,
@@ -154,70 +301,117 @@ module VCR
154
301
  end
155
302
  end
156
303
 
304
+ # Constructs a new instance from a hash.
305
+ #
306
+ # @param [Hash] hash the hash to use to construct the instance.
307
+ # @return [HTTPInteraction] the HTTP interaction
157
308
  def self.from_hash(hash)
158
309
  new Request.from_hash(hash.fetch('request', {})),
159
310
  Response.from_hash(hash.fetch('response', {})),
160
311
  Time.httpdate(hash.fetch('recorded_at'))
161
312
  end
162
313
 
163
- def ignore!
164
- @ignored = true
314
+ # @return [HookAware] an instance with additional capabilities
315
+ # suitable for use in `before_record` and `before_playback` hooks.
316
+ def hook_aware
317
+ HookAware.new(self)
165
318
  end
166
319
 
167
- def ignored?
168
- !!@ignored
169
- end
320
+ # Decorates an {HTTPInteraction} with additional methods useful
321
+ # for a `before_record` or `before_playback` hook.
322
+ class HookAware < DelegateClass(HTTPInteraction)
323
+ def initialize(http_interaction)
324
+ @ignored = false
325
+ super
326
+ end
170
327
 
171
- def filter!(text, replacement_text)
172
- return self if [text, replacement_text].any? { |t| t.to_s.empty? }
173
- filter_object!(self, text, replacement_text)
174
- end
328
+ # Flags the HTTP interaction so that VCR ignores it. This is useful in
329
+ # a {VCR::Configuration#before_record} or {VCR::Configuration#before_playback}
330
+ # hook so that VCR does not record or play it back.
331
+ # @see #ignored?
332
+ def ignore!
333
+ @ignored = true
334
+ end
175
335
 
176
- private
336
+ # @return [Boolean] whether or not this HTTP interaction should be ignored.
337
+ # @see #ignore!
338
+ def ignored?
339
+ !!@ignored
340
+ end
177
341
 
178
- def filter_object!(object, text, replacement_text)
179
- if object.respond_to?(:gsub)
180
- object.gsub!(text, replacement_text) if object.include?(text)
181
- elsif Hash === object
182
- filter_hash!(object, text, replacement_text)
183
- elsif object.respond_to?(:each)
184
- # This handles nested arrays and structs
185
- object.each { |o| filter_object!(o, text, replacement_text) }
342
+ # Replaces a string in any part of the HTTP interaction (headers, request body,
343
+ # response body, etc) with the given replacement text.
344
+ #
345
+ # @param [String] text the text to replace
346
+ # @param [String] replacement_text the text to put in its place
347
+ def filter!(text, replacement_text)
348
+ return self if [text, replacement_text].any? { |t| t.to_s.empty? }
349
+ filter_object!(self, text, replacement_text)
186
350
  end
187
351
 
188
- object
189
- end
352
+ private
190
353
 
191
- def filter_hash!(hash, text, replacement_text)
192
- filter_object!(hash.values, text, replacement_text)
354
+ def filter_object!(object, text, replacement_text)
355
+ if object.respond_to?(:gsub)
356
+ object.gsub!(text, replacement_text) if object.include?(text)
357
+ elsif Hash === object
358
+ filter_hash!(object, text, replacement_text)
359
+ elsif object.respond_to?(:each)
360
+ # This handles nested arrays and structs
361
+ object.each { |o| filter_object!(o, text, replacement_text) }
362
+ end
193
363
 
194
- hash.keys.each do |k|
195
- new_key = filter_object!(k.dup, text, replacement_text)
196
- hash[new_key] = hash.delete(k) unless k == new_key
364
+ object
365
+ end
366
+
367
+ def filter_hash!(hash, text, replacement_text)
368
+ filter_object!(hash.values, text, replacement_text)
369
+
370
+ hash.keys.each do |k|
371
+ new_key = filter_object!(k.dup, text, replacement_text)
372
+ hash[new_key] = hash.delete(k) unless k == new_key
373
+ end
197
374
  end
198
375
  end
199
376
  end
200
377
 
378
+ # The response of an {HTTPInteraction}.
379
+ #
380
+ # @attr [ResponseStatus] status the status of the response
381
+ # @attr [Hash{String => Array<String>}] headers the response headers
382
+ # @attr [String] body the response body
383
+ # @attr [nil, String] http_version the HTTP version
201
384
  class Response < Struct.new(:status, :headers, :body, :http_version)
202
385
  include Normalizers::Header
203
386
  include Normalizers::Body
204
387
 
388
+ # Builds a serializable hash from the response data.
389
+ #
390
+ # @return [Hash] hash that represents this response
391
+ # and can be easily serialized.
392
+ # @see Response.from_hash
205
393
  def to_hash
206
394
  {
207
395
  'status' => status.to_hash,
208
396
  'headers' => headers,
209
- 'body' => body,
397
+ 'body' => serializable_body,
210
398
  'http_version' => http_version
211
399
  }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
212
400
  end
213
401
 
402
+ # Constructs a new instance from a hash.
403
+ #
404
+ # @param [Hash] hash the hash to use to construct the instance.
405
+ # @return [Response] the response
214
406
  def self.from_hash(hash)
215
407
  new ResponseStatus.from_hash(hash.fetch('status', {})),
216
408
  hash['headers'],
217
- hash['body'],
409
+ body_from(hash['body']),
218
410
  hash['http_version']
219
411
  end
220
412
 
413
+ # Updates the Content-Length response header so that it is
414
+ # accurate for the response body.
221
415
  def update_content_length_header
222
416
  value = body ? body.bytesize.to_s : '0'
223
417
  key = %w[ Content-Length content-length ].find { |k| headers.has_key?(k) }
@@ -225,13 +419,26 @@ module VCR
225
419
  end
226
420
  end
227
421
 
422
+ # The response status of an {HTTPInteraction}.
423
+ #
424
+ # @attr [Integer] code the HTTP status code
425
+ # @attr [String] message the HTTP status message (e.g. "OK" for a status of 200)
228
426
  class ResponseStatus < Struct.new(:code, :message)
427
+ # Builds a serializable hash from the response status data.
428
+ #
429
+ # @return [Hash] hash that represents this response status
430
+ # and can be easily serialized.
431
+ # @see ResponseStatus.from_hash
229
432
  def to_hash
230
433
  {
231
434
  'code' => code, 'message' => message
232
435
  }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
233
436
  end
234
437
 
438
+ # Constructs a new instance from a hash.
439
+ #
440
+ # @param [Hash] hash the hash to use to construct the instance.
441
+ # @return [ResponseStatus] the response status
235
442
  def self.from_hash(hash)
236
443
  new hash['code'], hash['message']
237
444
  end