impersonator 0.1.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c27e2c12811df79ee6ecda2a617b9952ac98dff360ca3e08d725832d54cf5b03
4
- data.tar.gz: d40825fb2fbbdc7ac842bf58864acc2c02f2dd72b3a228b1ee0ee9ded86f891b
3
+ metadata.gz: 44894f5dac3e7080abf3ffebf05ea83a876394629f60f9e486319f8e4e5304e2
4
+ data.tar.gz: 994a96556cef09050e108cfd55539af2f61507afae6298a994da811f187076bb
5
5
  SHA512:
6
- metadata.gz: e8039d138b2f899a27d9476873ce687586a5322a7d0846a2828207592f51fd6a80f134a54fc7dce99cc8ea1d8581cdae25f62382e2129054d2148d1d05360543
7
- data.tar.gz: e39e557b268d09c24206d7db469fa7dd931002e0cf9b6b72b70e25ba2f87270f43b176170b43e83ae7b8fa0e26999c7e956afb1b5ac83a1fd2f0327359124e94
6
+ metadata.gz: 83d2e1d38817599af079e1160c519a9529a820485ee851e7d9934a897f3f81ba97f3dc6add6c4e3f59f73d88095442971f9f4231666337cc75d9dd1cc7468b5b
7
+ data.tar.gz: 7c8b47fb88e82aca2d80ffc27f09c1ae37842d0ca2a0356fdaed50bc9d5cb9bb659c6687759ea21c95949af215c16a8572b09dfc975e54c839461a17d71757a7
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ Style/FrozenStringLiteralComment:
4
4
  Enabled: false
5
5
 
6
6
  Metrics/LineLength:
7
- Enabled: false
7
+ Max: 105
8
8
 
9
9
  Metrics/AbcSize:
10
10
  Enabled: false
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2
4
+
5
+ - [Add YARD documentation](https://github.com/jorgemanrubia/impersonator/pull/3)
6
+
7
+ ## 0.1.1
8
+
9
+ - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- impersonator (0.1.1)
4
+ impersonator (0.1.2)
5
5
  zeitwerk (~> 2.1.6)
6
6
 
7
7
  GEM
@@ -41,7 +41,7 @@ GEM
41
41
  rubocop (>= 0.60.0)
42
42
  ruby-progressbar (1.10.1)
43
43
  unicode-display_width (1.6.0)
44
- zeitwerk (2.1.6)
44
+ zeitwerk (2.1.8)
45
45
 
46
46
  PLATFORMS
47
47
  ruby
data/impersonator.gemspec CHANGED
@@ -9,7 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['jorge.manrubia@gmail.com']
10
10
 
11
11
  spec.summary = 'Generate test stubs that replay recorded interactions'
12
- spec.description = 'Record and replay object interactions. Ideal for mocking not-http services when testing (just because, for http, VCR is probably what you want)'
12
+ spec.description = 'Record and replay object interactions. Ideal for mocking not-http services'\
13
+ ' when testing (just because, for http, VCR is probably what you want)'
13
14
  spec.homepage = 'https://github.com/jorgemanrubia/impersonator'
14
15
  spec.license = 'MIT'
15
16
 
@@ -1,7 +1,16 @@
1
1
  module Impersonator
2
+ # Public API exposed by the global `Impersonator` module.
2
3
  module Api
4
+ # Wraps the execution of the yielded code withing a new {Recording recording} titled with the
5
+ # passed label.
6
+ #
7
+ # @param [String] label The label for the recording
8
+ # @param [Boolean] disabled `true` will disable replay mode and always execute code in *record*
9
+ # mode. `false` by default
3
10
  def recording(label, disabled: false)
4
- @current_recording = ::Impersonator::Recording.new(label, disabled: disabled, recordings_path: configuration.recordings_path)
11
+ @current_recording = ::Impersonator::Recording.new label,
12
+ disabled: disabled,
13
+ recordings_path: configuration.recordings_path
5
14
  @current_recording.start
6
15
  yield
7
16
  @current_recording.finish
@@ -9,14 +18,28 @@ module Impersonator
9
18
  @current_recording = nil
10
19
  end
11
20
 
21
+ # The current recording, if any, or `nil` otherwise.
22
+ #
23
+ # @return [Recording, nil]
12
24
  def current_recording
13
25
  @current_recording
14
26
  end
15
27
 
28
+ # Configures how Impersonator works by yielding a {Configuration configuration} object
29
+ # you can use to tweak settings.
30
+ #
31
+ # ```
32
+ # Impersonator.configure do |config|
33
+ # config.recordings_path = 'my/own/recording/path'
34
+ # end
35
+ # ```
36
+ #
37
+ # @yieldparam config [Configuration]
16
38
  def configure
17
39
  yield configuration
18
40
  end
19
41
 
42
+ # @return [Configuration]
20
43
  def configuration
21
44
  @configuration ||= Configuration.new
22
45
  end
@@ -36,14 +59,17 @@ module Impersonator
36
59
  # impersonator = Impersonator.impersonate(:add, :subtract) { Calculator.new }
37
60
  # impersonator.add(3, 4)
38
61
  #
39
- # Notice that the actual object won't be instantiated in record mode. For that reason, the impersonated
40
- # object will only respond to the list of impersonated methods.
62
+ # Notice that the actual object won't be instantiated in record mode. For that reason, the
63
+ # impersonated object will only respond to the list of impersonated methods.
41
64
  #
42
65
  # If you need to invoke other (not impersonated) methods see #impersonate_method instead.
43
66
  #
44
- # @return [Object] the impersonated object
67
+ # @param [Array<Symbols, Strings>] methods list of methods to impersonate
68
+ # @return [Proxy] the impersonated proxy object
45
69
  def impersonate(*methods)
46
- raise ArgumentError, 'Provide a block to instantiate the object to impersonate in record mode' unless block_given?
70
+ unless block_given?
71
+ raise ArgumentError, 'Provide a block to instantiate the object to impersonate in record mode'
72
+ end
47
73
 
48
74
  object_to_impersonate = if current_recording&.record_mode?
49
75
  yield
@@ -55,14 +81,20 @@ module Impersonator
55
81
 
56
82
  # Impersonates a list of methods of a given object
57
83
  #
58
- # The returned object will impersonate the list of methods and will delegate the rest of method calls
59
- # to the actual object.
84
+ # The returned object will impersonate the list of methods and will delegate the rest of method
85
+ # calls to the actual object.
60
86
  #
61
- # @return [Object] the impersonated object
87
+ # @param [Object] actual_object The actual object to impersonate
88
+ # @param [Array<Symbols, Strings>] methods list of methods to impersonate
89
+ # @return [Proxy] the impersonated proxy object
62
90
  def impersonate_methods(actual_object, *methods)
63
- raise Impersonator::Errors::ConfigurationError, 'You must start a recording to impersonate objects. Use Impersonator.recording {}' unless @current_recording
91
+ unless @current_recording
92
+ raise Impersonator::Errors::ConfigurationError, 'You must start a recording to impersonate'\
93
+ ' objects. Use Impersonator.recording {}'
94
+ end
64
95
 
65
- ::Impersonator::Proxy.new(actual_object, recording: current_recording, impersonated_methods: methods)
96
+ ::Impersonator::Proxy.new(actual_object, recording: current_recording,
97
+ impersonated_methods: methods)
66
98
  end
67
99
  end
68
100
  end
@@ -1,3 +1,4 @@
1
1
  module Impersonator
2
+ # A block invocation
2
3
  BlockInvocation = Struct.new(:arguments, keyword_init: true)
3
4
  end
@@ -1,5 +1,7 @@
1
1
  module Impersonator
2
+ # An spy object that can collect {BlockInvocation block invocations}
2
3
  BlockSpy = Struct.new(:block_invocations, :actual_block, keyword_init: true) do
4
+ # @return [Proc] a proc that will collect {BlockInvocation block invocations}
3
5
  def block
4
6
  @block ||= proc do |*arguments|
5
7
  self.block_invocations ||= []
@@ -1,5 +1,8 @@
1
1
  module Impersonator
2
+ # General configuration settings for Impersonator
2
3
  Configuration = Struct.new(:recordings_path, keyword_init: true) do
4
+ # @!attribute recordings_path [String] The path where recordings are saved to
5
+
3
6
  DEFAULT_RECORDINGS_FOLDER = 'recordings'.freeze
4
7
 
5
8
  def initialize(*)
@@ -1,5 +1,7 @@
1
1
  module Impersonator
2
+ # A simple double implementation. It will generate empty stubs for the passed list of methods
2
3
  class Double
4
+ # @param [Array<String, Symbol>] methods The list of methods this double will respond to
3
5
  def initialize(*methods)
4
6
  define_methods(methods)
5
7
  end
@@ -1,5 +1,6 @@
1
1
  module Impersonator
2
2
  module Errors
3
+ # Indicates a configuration error
3
4
  class ConfigurationError < StandardError
4
5
  end
5
6
  end
@@ -1,5 +1,6 @@
1
1
  module Impersonator
2
2
  module Errors
3
+ # Unexpected method invocation error
3
4
  class MethodInvocationError < StandardError
4
5
  end
5
6
  end
@@ -1,4 +1,7 @@
1
1
  module Impersonator
2
+ # A mixin that will add a method `logger` to access a logger instance
3
+ #
4
+ # @see ::Impersonator.logger
2
5
  module HasLogger
3
6
  def logger
4
7
  ::Impersonator.logger
@@ -1,5 +1,12 @@
1
1
  module Impersonator
2
+ # A method instance
2
3
  Method = Struct.new(:name, :arguments, :block, :matching_configuration, keyword_init: true) do
4
+ # @!attribute name [String] Method name
5
+ # @!attribute arguments [Array<Object>] Arguments passed to the method invocation
6
+ # @!attribute arguments [#call] The block passed to the method
7
+ # @!attribute matching_configuration [MethodMatchingConfiguration] The configuration that will
8
+ # be used to match the method invocation at replay mode
9
+
3
10
  def to_s
4
11
  string = name.to_s
5
12
 
@@ -10,6 +17,9 @@ module Impersonator
10
17
  string
11
18
  end
12
19
 
20
+ # The spy used to spy the block yield invocations
21
+ #
22
+ # @return [BlockSpy]
13
23
  def block_spy
14
24
  return nil if !@block_spy && !block
15
25
 
@@ -38,7 +48,8 @@ module Impersonator
38
48
  other_arguments.delete_at(ignored_position)
39
49
  end
40
50
 
41
- name == other_method.name && my_arguments == other_arguments && !block_spy == !other_method.block_spy
51
+ name == other_method.name && my_arguments == other_arguments &&
52
+ !block_spy == !other_method.block_spy
42
53
  end
43
54
  end
44
55
  end
@@ -1,3 +1,4 @@
1
1
  module Impersonator
2
+ # A method invocation groups a {Method method instance} and a return value
2
3
  MethodInvocation = Struct.new(:method_instance, :return_value, keyword_init: true)
3
4
  end
@@ -1,4 +1,5 @@
1
1
  module Impersonator
2
+ # Configuration options for matching methods
2
3
  class MethodMatchingConfiguration
3
4
  attr_reader :ignored_positions
4
5
 
@@ -6,6 +7,9 @@ module Impersonator
6
7
  @ignored_positions = []
7
8
  end
8
9
 
10
+ # Configure positions to ignore
11
+ #
12
+ # @param [Array<Integer>] positions The positions of arguments to ignore (0 being the first one)
9
13
  def ignore_arguments_at(*positions)
10
14
  ignored_positions.push(*positions)
11
15
  end
@@ -1,9 +1,17 @@
1
1
  module Impersonator
2
+ # A proxy represents the impersonated object at both record and replay times.
3
+ #
4
+ # For not impersonated methods, it will just delegate to the impersonate object. For impersonated
5
+ # methods, it will interact with the {Recording recording} for recording or replaying the object
6
+ # interactions.
2
7
  class Proxy
3
8
  include HasLogger
4
9
 
5
10
  attr_reader :impersonated_object
6
11
 
12
+ # @param [Object] impersonated_object
13
+ # @param [Recording] recording
14
+ # @param [Array<Symbol, String>] impersonated_methods The methods to impersonate
7
15
  def initialize(impersonated_object, recording:, impersonated_methods:)
8
16
  validate_object_has_methods_to_impersonate!(impersonated_object, impersonated_methods)
9
17
 
@@ -25,6 +33,16 @@ module Impersonator
25
33
  impersonated_object.respond_to_missing?(method_name, *args)
26
34
  end
27
35
 
36
+ # Configure matching options for a given method
37
+ #
38
+ # ```ruby
39
+ # impersonator.configure_method_matching_for(:add) do |config|
40
+ # config.ignore_arguments_at 0
41
+ # end
42
+ # ```
43
+ #
44
+ # @param [String, Symbol] method The method to configure matching options for
45
+ # @yieldparam config [MethodMatchingConfiguration]
28
46
  def configure_method_matching_for(method)
29
47
  method_matching_configurations_by_method[method.to_sym] ||= MethodMatchingConfiguration.new
30
48
  yield method_matching_configurations_by_method[method]
@@ -39,15 +57,21 @@ module Impersonator
39
57
  !object.respond_to?(method.to_sym)
40
58
  end
41
59
 
42
- raise Impersonator::Errors::ConfigurationError, "These methods to impersonate does not exist: #{missing_methods.inspect}" unless missing_methods.empty?
60
+ unless missing_methods.empty?
61
+ raise Impersonator::Errors::ConfigurationError, 'These methods to impersonate does not'\
62
+ "exist: #{missing_methods.inspect}"
63
+ end
43
64
  end
44
65
 
45
66
  def invoke_impersonated_method(method_name, *args, &block)
46
- method = Method.new(name: method_name, arguments: args, block: block, matching_configuration: method_matching_configurations_by_method[method_name.to_sym])
67
+ matching_configuration = method_matching_configurations_by_method[method_name.to_sym]
68
+ method = Method.new(name: method_name, arguments: args, block: block,
69
+ matching_configuration: matching_configuration)
47
70
  if recording.replay_mode?
48
71
  recording.replay(method)
49
72
  else
50
- @impersonated_object.send(method_name, *args, &method&.block_spy&.block).tap do |return_value|
73
+ spiable_block = method&.block_spy&.block
74
+ @impersonated_object.send(method_name, *args, &spiable_block).tap do |return_value|
51
75
  recording.record(method, return_value)
52
76
  end
53
77
  end
@@ -1,17 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Impersonator
4
+ # A recording is responsible for saving interactions at record time, and replaying them at
5
+ # replay time.
4
6
  class Recording
5
7
  include HasLogger
6
8
 
7
9
  attr_reader :label
8
10
 
11
+ # @param [String] label
12
+ # @param [Boolean] disabled `true` for always working in *record* mode. `false` by default
13
+ # @param [String] the path to save recordings to
9
14
  def initialize(label, disabled: false, recordings_path:)
10
15
  @label = label
11
16
  @recordings_path = recordings_path
12
17
  @disabled = disabled
13
18
  end
14
19
 
20
+ # Start a recording/replay session
15
21
  def start
16
22
  logger.debug "Starting recording #{label}..."
17
23
  if can_replay?
@@ -21,15 +27,23 @@ module Impersonator
21
27
  end
22
28
  end
23
29
 
30
+ # Record a {MethodInvocation method invocation} with a given return value
31
+ # @param [Method] method
32
+ # @param [Object] return_value
24
33
  def record(method, return_value)
25
34
  method_invocation = MethodInvocation.new(method_instance: method, return_value: return_value)
26
35
 
27
36
  @method_invocations << method_invocation
28
37
  end
29
38
 
39
+ # Replay a method invocation
40
+ # @param [Method] method
30
41
  def replay(method)
31
42
  method_invocation = @method_invocations.shift
32
- raise Impersonator::Errors::MethodInvocationError, "Unexpected method invocation received: #{method}" unless method_invocation
43
+ unless method_invocation
44
+ raise Impersonator::Errors::MethodInvocationError, 'Unexpected method invocation received:'\
45
+ "#{method}"
46
+ end
33
47
 
34
48
  validate_method_signature!(method, method_invocation.method_instance)
35
49
  replay_block(method_invocation, method)
@@ -37,6 +51,7 @@ module Impersonator
37
51
  method_invocation.return_value
38
52
  end
39
53
 
54
+ # Finish a record/replay session.
40
55
  def finish
41
56
  logger.debug "Recording #{label} finished"
42
57
  if record_mode?
@@ -46,10 +61,16 @@ module Impersonator
46
61
  end
47
62
  end
48
63
 
64
+ # Return whether it is currently at replay mode
65
+ #
66
+ # @return [Boolean]
49
67
  def replay_mode?
50
68
  @replay_mode
51
69
  end
52
70
 
71
+ # Return whether it is currently at record mode
72
+ #
73
+ # @return [Boolean]
53
74
  def record_mode?
54
75
  !replay_mode?
55
76
  end
@@ -88,8 +109,9 @@ module Impersonator
88
109
 
89
110
  def finish_in_replay_mode
90
111
  unless @method_invocations.empty?
91
- raise Impersonator::Errors::MethodInvocationError, "Expecting #{@method_invocations.length} method invocations"\
92
- " that didn't happen: #{@method_invocations.inspect}"
112
+ raise Impersonator::Errors::MethodInvocationError,
113
+ "Expecting #{@method_invocations.length} method invocations"\
114
+ " that didn't happen: #{@method_invocations.inspect}"
93
115
  end
94
116
  end
95
117
 
@@ -1,3 +1,3 @@
1
1
  module Impersonator
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.1.2'.freeze
3
3
  end
data/lib/impersonator.rb CHANGED
@@ -11,6 +11,9 @@ loader.setup
11
11
  module Impersonator
12
12
  extend Api
13
13
 
14
+ # The gem logger instance
15
+ #
16
+ # @return [::Logger]
14
17
  def self.logger
15
18
  @logger ||= ::Logger.new(STDOUT).tap do |logger|
16
19
  logger.level = Logger::WARN
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: impersonator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorge Manrubia
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-18 00:00:00.000000000 Z
11
+ date: 2019-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -94,6 +94,8 @@ files:
94
94
  - ".rubocop.yml"
95
95
  - ".ruby-version"
96
96
  - ".travis.yml"
97
+ - ".yardopts"
98
+ - CHANGELOG.md
97
99
  - Gemfile
98
100
  - Gemfile.lock
99
101
  - LICENSE.txt