impersonator 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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