around_the_world 0.4.3 → 0.5.0

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: bf3d2cd621e18996b29ef47dc0fd22a933cf6fa4add557b04a1cecbafbd6ae49
4
- data.tar.gz: 2584124852e8f1d30b240bdd59da3ddebe92f1e553b72cf187be45030400f7e0
3
+ metadata.gz: 102af00bd8cee2883f110fbdc21569c3d28d5a6f3ddf991f9d0ff61e6e495ce9
4
+ data.tar.gz: 65e03179f83fb8e7fce01dc8d5656c6270de7a4cea41b0abfd21fa440f984803
5
5
  SHA512:
6
- metadata.gz: 97f1643c1942aef0020fc6c3d82deb5e75bf871f927d9571ddb898aa34816b6ee2b0c756089855e323b35ce89176a1ad8f54bc5fb70ca912f867e2037cd1c28b
7
- data.tar.gz: be6f2f090b1c17a1e1dd094f030393400eddccdac6ac39d159427c9111fdc5ec1afd9475073ac380ca16243734d78194da1e256a3442dc1a2967c0f4cb41e0f6
6
+ metadata.gz: 463f3e58ebc0cf77787711929a010cc1f1e9750cf39a8a3d88da9f29c19df982dfb72efa6b09ce852097a731afa906ad557f8b35170fc0a4895966337d52ab6c
7
+ data.tar.gz: 5597d4aa0d6f958429546e8ccd6e8607565ea59b09d8d63255ae60e139da1018d5a2f47b46cb78e5056876c19e0117c3c8960bf5a916f708632728f75b09aeb3
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "around_the_world/version"
4
3
  require_relative "around_the_world/errors"
4
+ require_relative "around_the_world/method_wrapper"
5
+ require_relative "around_the_world/proxy_module"
6
+ require_relative "around_the_world/version"
5
7
  require "active_support/concern"
6
8
 
7
9
  module AroundTheWorld
@@ -14,7 +16,7 @@ module AroundTheWorld
14
16
  #
15
17
  # @example
16
18
  # class SomeClass
17
- # around_method :dont_look_in_here, "DoesAThing" do
19
+ # around_method :dont_look_in_here do
18
20
  # things_happened = super
19
21
  #
20
22
  # if things_happened
@@ -32,36 +34,46 @@ module AroundTheWorld
32
34
  # SomeClass.new.dont_look_in_here
33
35
  # => "Something happened!"
34
36
  #
37
+ # @example
38
+ # around_method :dont_look_in_here, prevent_double_wrapping_for: :memoization do
39
+ # @memoized ||= super
40
+ # end
41
+ #
42
+ # around_method :dont_look_in_here, prevent_double_wrapping_for: :memoization do
43
+ # @memoized ||= super
44
+ # end
45
+ # # => AroundTheWorld::DoubleWrapError:
46
+ # "Module AroundTheWorld:ProxyModule:memoization already defines the method :dont_look_in_here"
47
+ #
48
+ # around_method :dont_look_in_here do
49
+ # do_something_else
50
+ # super
51
+ # end
52
+ # # => no error raised
53
+ #
35
54
  # @param method_name [Symbol]
36
- # @param proxy_module_name [String] The camelized name of a custom module to place the wrapper method in.
37
- # This is necessary to enable wrapping a single method more than once
38
- # since a module cannot super to itself.
39
- # It's recommended to name the module after what the method wrapper will do,
40
- # for example LogsAnEvent for a wrapper method that logs something.
41
- # Because of the potential for overriding previously wrapped methods, this
42
- # parameter is required.
43
- def around_method(method_name, proxy_module_name, &block)
44
- proxy_module = around_method_proxy_module(proxy_module_name)
45
- ensure_around_method_uniqueness!(method_name, proxy_module)
46
-
47
- proxy_module.define_method(method_name, &block)
48
- prepend proxy_module unless ancestors.include?(proxy_module)
49
- end
50
-
51
- private
52
-
53
- def around_method_proxy_module(proxy_module_name)
54
- namespaced_proxy_module_name = "#{self}::#{proxy_module_name}"
55
-
56
- const_set(proxy_module_name, Module.new) unless const_defined?(namespaced_proxy_module_name)
57
-
58
- const_get(namespaced_proxy_module_name)
59
- end
55
+ # @param proxy_module_name [String]
56
+ # DEPRECATED: this argument is deprecated and will be removed in a future release.
57
+ # Use the :prevent_double_wrapping_for option instead.
58
+ # The camelized name of a custom module to place the wrapper method in. This is necessary
59
+ # to enable wrapping a single method more than once since a module cannot super to itself.
60
+ # It's recommended to name the module after what the method wrapper will do, for example
61
+ # LogsAnEvent for a wrapper method that logs something. Because of the potential for
62
+ # overriding previously wrapped methods, this parameter is required.
63
+ #
64
+ # @param :prevent_double_wrapping_for [Object]
65
+ # If defined, this prevents wrapping the method twice for a given purpose. Accepts any argument.
66
+ def around_method(method_name, proxy_module_name = nil, prevent_double_wrapping_for: nil, &block)
67
+ if proxy_module_name
68
+ prevent_double_wrapping_for ||= proxy_module_name
60
69
 
61
- def ensure_around_method_uniqueness!(method_name, proxy_module)
62
- return unless proxy_module.instance_methods.include?(method_name.to_sym)
70
+ puts <<~DEPRECATION
71
+ DEPRECATION WARNING: the proxy_module_name argument is deprecated and will be removed
72
+ in version 1.0. Please use the :prevent_double_wrapping_for option instead.
73
+ DEPRECATION
74
+ end
63
75
 
64
- raise DoubleWrapError, "Module #{proxy_module} already defines the method :#{method_name}"
76
+ MethodWrapper.wrap(method_name, self, prevent_double_wrapping_for, &block)
65
77
  end
66
78
  end
67
79
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AroundTheWorld
4
+ class MethodNotDefinedError < NoMethodError; end
4
5
  class DoubleWrapError < StandardError; end
5
6
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AroundTheWorld
4
+ class MethodWrapper
5
+ class << self
6
+ # Passes arguments directly to {#new} - see docs
7
+ def wrap(method_name, target, prevent_double_wrapping_for = false, &block)
8
+ new(method_name, target, prevent_double_wrapping_for, &block).wrap
9
+ end
10
+ end
11
+
12
+ attr_reader :method_name, :target, :prevent_double_wrapping_for, :block
13
+
14
+ # @param method_name [String, Symbol] The name of the method to be wrapped.
15
+ # @param target [Module] The class or module containing the method to be wrapped.
16
+ # @param prevent_double_wrapping_for [String, Symbol]
17
+ # An identifier to define the proxy module's purpose in the ancestor tree.
18
+ # A method can only be wrapped once for a given purpose, though it can be wrapped
19
+ # again for other purposes, or for no given purpose.
20
+ # @block The block that will be executed when the method is invoked.
21
+ # Should always call super, at least conditionally.
22
+ def initialize(method_name, target, prevent_double_wrapping_for = false, &block)
23
+ @method_name = method_name.to_sym
24
+ @target = target
25
+ @prevent_double_wrapping_for = prevent_double_wrapping_for
26
+ @block = block
27
+ end
28
+
29
+ # Defines the wrapped method inside a proxy module and prepends the proxy module to the target module if necessary.
30
+ def wrap
31
+ ensure_method_defined!
32
+ prevent_double_wrapping! if prevent_double_wrapping?
33
+
34
+ define_proxy_method
35
+ target.prepend proxy_module unless target.ancestors.include?(proxy_module)
36
+ end
37
+
38
+ private
39
+
40
+ def ensure_method_defined!
41
+ return if target.instance_methods(true).include?(method_name) || target.private_method_defined?(method_name)
42
+
43
+ raise MethodNotDefinedError, "#{target} does not define :#{method_name}"
44
+ end
45
+
46
+ def prevent_double_wrapping!
47
+ return unless proxy_module_for(prevent_double_wrapping_for)&.instance_methods&.include?(method_name)
48
+
49
+ raise DoubleWrapError, "Module #{proxy_module} already defines the method :#{method_name}"
50
+ end
51
+
52
+ def define_proxy_method
53
+ proxy_module.define_method(method_name, &block)
54
+
55
+ proxy_module.instance_exec(method_name, method_privacy) do |method_name, method_privacy|
56
+ case method_privacy
57
+ when :protected
58
+ protected method_name
59
+ when :private
60
+ private method_name
61
+ end
62
+ end
63
+ end
64
+
65
+ def method_privacy
66
+ if target.protected_method_defined?(method_name)
67
+ :protected
68
+ elsif target.private_method_defined?(method_name)
69
+ :private
70
+ end
71
+ end
72
+
73
+ def prevent_double_wrapping?
74
+ !prevent_double_wrapping_for.blank?
75
+ end
76
+
77
+ # @return [AroundTheWorld::ProxyModule] The proxy module upon which the method wrapper will be defined
78
+ def proxy_module
79
+ return @proxy_module ||= proxy_module_for(prevent_double_wrapping_for) if prevent_double_wrapping?
80
+
81
+ @proxy_module ||= first_available_proxy_module || ProxyModule.new
82
+ end
83
+
84
+ # @param purpose [String, Symbol] An identifier to define the proxy module's purpose in the ancestor tree.
85
+ # @return [AroundTheWorld::ProxyModule] Either an already-defined proxy module for the given purpose,
86
+ # or a new proxy module if one does not exist for the gibven purpose.
87
+ def proxy_module_for(purpose)
88
+ raise ArgumentError if purpose.blank?
89
+
90
+ existing_proxy_modules.find { |proxy_module| proxy_module.for?(purpose) } ||
91
+ ProxyModule.new(purpose: prevent_double_wrapping_for)
92
+ end
93
+
94
+ # @return [AroundTheWorld::ProxyModule] The first defined ProxyModule that does not define the method to be wrapped.
95
+ # @return [NilClass] If all prepended ProxyModules already define the method,
96
+ # or no ProxyModules have been prepended yet.
97
+ def first_available_proxy_module
98
+ existing_proxy_modules.reverse_each.find { |ancestor| !ancestor.instance_methods.include?(method_name) }
99
+ end
100
+
101
+ # @return [Array<AroundTheWorld::ProxyModule>] All ProxyModules +prepended+ to the target module.
102
+ def existing_proxy_modules
103
+ @existing_proxy_modules ||= target.ancestors.select.with_index do |ancestor, index|
104
+ next if index >= base_ancestry_index
105
+
106
+ ancestor.is_a? ProxyModule
107
+ end
108
+ end
109
+
110
+ # @return [Integer] The index of the target module in its ancestry array
111
+ def base_ancestry_index
112
+ @base_ancestry_index = target.ancestors.index(target)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AroundTheWorld
4
+ class ProxyModule < Module
5
+ attr_reader :purpose
6
+
7
+ def initialize(purpose: nil)
8
+ @purpose = purpose unless purpose.blank?
9
+ end
10
+
11
+ def for?(purpose)
12
+ return false if self.purpose.blank?
13
+
14
+ self.purpose == purpose
15
+ end
16
+
17
+ def inspect
18
+ "#<#{self.class.name}#{":#{purpose}" if purpose}>"
19
+ end
20
+ end
21
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module AroundTheWorld
4
4
  # This constant is managed by spicerack
5
- VERSION = "0.4.3"
5
+ VERSION = "0.5.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: around_the_world
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Allen Rettberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-10 00:00:00.000000000 Z
11
+ date: 2018-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -35,6 +35,8 @@ files:
35
35
  - README.md
36
36
  - lib/around_the_world.rb
37
37
  - lib/around_the_world/errors.rb
38
+ - lib/around_the_world/method_wrapper.rb
39
+ - lib/around_the_world/proxy_module.rb
38
40
  - lib/around_the_world/version.rb
39
41
  homepage: https://www.freshly.com
40
42
  licenses: