duck_puncher 3.0.0 → 4.0.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
  SHA1:
3
- metadata.gz: 6050f09ba1e9b843078f3d6eb434b668da5687a8
4
- data.tar.gz: 30a1083c6ccb5edfa9520e3f395b63c65b221040
3
+ metadata.gz: f945aea9eb0db7392e51aea5cda254eaad6d5815
4
+ data.tar.gz: fa1e7a2565caee23e437d38b60358f9fbf382901
5
5
  SHA512:
6
- metadata.gz: fa163f85c3ec46d0a3071a4d9e75b3c2de14b37bcd5f29b8b8fe3b65d13e617f37f4740894e4fc393482c75a9aedeb25600da618e74469fc30341428512c7001
7
- data.tar.gz: 86d3f879aba2e9e78694398d348a7b18e909931584dedf65617538b12e5c88344ef5b0bf0e711fc8899f7a1683e26d9b42400dd293803318d8e10b4371e618df
6
+ metadata.gz: 3c05f53aad5478fda527751c1cc3625cf6d0dad18397a08311610d56a13ada9fab6705623f7ae8fec63902636f78255cee3ad3fd892e9b0ada44c9132bba3d3e
7
+ data.tar.gz: b7310b2c603075ce345f24805bb4d2dc7211515c86b69dc0df775d8aa362cf723c656b3a22a8a0696d0a2fd1b6294e7b96c6a91511d26eb35ea61956865f74c4
data/README.md CHANGED
@@ -3,29 +3,34 @@
3
3
  DuckPuncher provides an interface for administering __duck punches__ (a.k.a "monkey patches"). Punches can be administered in several ways:
4
4
 
5
5
  * as an extension
6
- * as a subclass
6
+ * as a decorator
7
7
 
8
8
  Default extensions:
9
9
 
10
10
  ```ruby
11
- Array #m => `[].m(:to_s)` => `[].map(&:to_s)`
12
- #m! => `[].m!(:upcase)` => `[].map!(&:upcase)`
13
- #mm => `[].mm(:sub, /[aeiou]/, '*')` => `[].map { |x| x.sub(/[aeiou]/, '*') }`
14
- #mm! => `[].mm!(:sub, /[aeiou]/, '*')` => `[].map! { |x| x.sub(/[aeiou]/, '*') }`
15
- #except => `[].except('foo', 'bar')` => `[] - ['foo', 'bar']`
16
- Hash #dig => `{a: 1, b: {c: 2}}.dig(:b, :c)` => 2 (Part of standard lib in Ruby >= 2.3)
17
- Numeric #to_currency => `25.245.to_currency` => 25.25
18
- #to_duration => `10_000.to_duration` => '2 h 46 min'
19
- #to_time_ago => `10_000.to_time_ago` => '2 hours ago'
20
- #to_rad => `10.15.to_rad` => 0.17715091907742445
21
- String #pluralize => `'hour'.pluralize(2)` => "hours"
22
- #underscore => `'DuckPuncher::JSONStorage'.underscore` => 'duck_puncher/json_storage'
23
- #to_boolean => `'1'.to_boolean` => true
24
- #constantize => `'MiniTest::Test'.constantize` => MiniTest::Test
25
- Object #clone! => `Object.new.clone!` => a deep clone of the object (using Marshal.dump)
26
- #punch => `'duck'.punch` => a copy of 'duck' with String punches mixed in
27
- Method #to_instruct => `Benchmark.method(:measure).to_instruct` returns the Ruby VM instruction sequence for the method
28
- #to_source => `Benchmark.method(:measure).to_source` returns the method definition as a string
11
+ Array #m => `[].m(:to_s)` => `[].map(&:to_s)`
12
+ #m! => `[].m!(:upcase)` => `[].map!(&:upcase)`
13
+ #mm => `[].mm(:sub, /[aeiou]/, '*')` => `[].map { |x| x.sub(/[aeiou]/, '*') }`
14
+ #mm! => `[].mm!(:sub, /[aeiou]/, '*')` => `[].map! { |x| x.sub(/[aeiou]/, '*') }`
15
+ #except => `[].except('foo', 'bar')` => `[] - ['foo', 'bar']`
16
+ Hash #dig => `{a: 1, b: {c: 2}}.dig(:b, :c)` => 2 (Part of standard lib in Ruby >= 2.3)
17
+ #compact => `{a: 1, b: nil}.compact` => {a: 1}
18
+ Numeric #to_currency => `25.245.to_currency` => 25.25
19
+ #to_duration => `10_000.to_duration` => '2 h 46 min'
20
+ #to_time_ago => `10_000.to_time_ago` => '2 hours ago'
21
+ #to_rad => `10.15.to_rad` => 0.17715091907742445
22
+ String #pluralize => `'hour'.pluralize(2)` => "hours"
23
+ #underscore => `'DuckPuncher::JSONStorage'.underscore` => 'duck_puncher/json_storage'
24
+ #to_boolean => `'1'.to_boolean` => true
25
+ #constantize => `'MiniTest::Test'.constantize` => MiniTest::Test
26
+ Module #local_methods => `Kernel.local_methods` returns the methods defined directly in the class + nested constants w/ methods
27
+ Object #clone! => `Object.new.clone!` => a deep clone of the object (using Marshal.dump)
28
+ #punch => `'duck'.punch` => a copy of 'duck' with String punches mixed in
29
+ #punch! => `'duck'.punch!` => destructive version applies extensions directly to the base object
30
+ #echo => `'duck'.echo.upcase` => spits out the caller and value of the object and returns the object
31
+ #track => `Object.new.track` => Traces methods calls to the object (requires [object_tracker](https://github.com/ridiculous/object_tracker), which it'll try to download)
32
+ Method #to_instruct => `Benchmark.method(:measure).to_instruct` returns the Ruby VM instruction sequence for the method
33
+ #to_source => `Benchmark.method(:measure).to_source` returns the method definition as a string
29
34
  ```
30
35
 
31
36
  ## Usage
@@ -39,13 +44,13 @@ DuckPuncher.punch_all!
39
44
  Punch individual ducks by name:
40
45
 
41
46
  ```ruby
42
- DuckPuncher.punch! :Hash, :Object
47
+ DuckPuncher.punch! Hash, Object
43
48
  ```
44
49
 
45
50
  One method to rule them all:
46
51
 
47
52
  ```ruby
48
- DuckPuncher.punch! :Object, only: :punch
53
+ DuckPuncher.punch! Object, only: :punch
49
54
  ```
50
55
 
51
56
  ### Tactical punches
@@ -53,34 +58,55 @@ DuckPuncher.punch! :Object, only: :punch
53
58
  `DuckPuncher` extends the amazing [Usable](https://github.com/ridiculous/usable) gem, so you can configure only the punches you want! For instance:
54
59
 
55
60
  ```ruby
56
- DuckPuncher.punch! :Numeric, only: [:to_currency, :to_duration]
61
+ DuckPuncher.punch! Numeric, only: [:to_currency, :to_duration]
57
62
  ```
58
63
 
59
- Use `DuckPuncher.punch` to create a new class that __inherits__ from the original (automatically cached for future calls):
64
+ If you punch `Object` then you can use `#punch!` on any object to extend individual instances:
60
65
 
61
66
  ```ruby
62
- >> DuckPuncher.punch :String
63
- => DuckPuncher::StringDuck
64
- >> DuckPuncher::StringDuck.new('Yes').to_boolean
65
- => true
66
- >> String.new('Yes').respond_to? :to_boolean
67
- => false
67
+ >> DuckPuncher.punch! Object, only: :punch!
68
+ >> %w[yes no 1].punch!.m!(:punch).m(:to_boolean)
69
+ => [true, false, true]
68
70
  ```
69
71
 
70
- If you punch `Object` then you can use `#punch` on any object to extend individual instances:
71
-
72
+ Alternatively, there is also the `Object#punch` method which returns a decorated copy of an object with punches mixed in:
72
73
  ```ruby
73
- >> DuckPuncher.punch! :Object, only: :punch
74
- >> %w[yes no 1].punch.m!(:punch).m(:to_boolean)
75
- => [true, false, true]
74
+ >> DuckPuncher.punch! Object, only: :punch
75
+ >> %w[1 2 3].punch.m(:to_i)
76
+ => [1, 2, 3]
76
77
  ```
77
78
 
78
- The `#punch` method will lookup the extension by the object's class name. The above example works because `:Array` and `:String` are default extensions. If you want to punch a specific extension, then you can specify it as an argument:
79
+ The `#punch!` method will lookup the extension by the object's class name. The above example works because `Array` and `String` are default extensions. If you want to punch a specific extension, then you can specify it as an argument:
79
80
  ```ruby
80
81
  >> LovableDuck = Module.new { def inspect() "I love #{self.first}" end }
81
- >> DuckPuncher.register :with_love, mod: 'LovableDuck'
82
- >> %w[ducks].punch(:with_love)
82
+ >> DuckPuncher.register Array, LovableDuck
83
+ >> ducks = %w[ducks]
84
+ >> soft_punch = ducks.punch
83
85
  => "I love ducks"
86
+ >> soft_punch.class
87
+ => DuckPuncher::ArrayDelegator
88
+ >> ducks.punch!.class
89
+ => Array
90
+ ```
91
+
92
+ When there are no punches registered for a class, it'll search the ancestor list for a class with registered punches. For example, `Array` doesn't have
93
+ a method defined `echo`, but when we punch `Object`, it means all subclasses have access to the same methods, even with soft punches.
94
+
95
+ ```ruby
96
+ def soft_punch
97
+ ('a'..'z').punch.echo.to_a.map(&:upcase)
98
+ end
99
+
100
+ def hard_punch
101
+ ('a'..'z').to_a.punch!.m!(:upcase).mm!(:*, 3).echo
102
+ end
103
+
104
+ >> soft_punch
105
+ "a..z -- (irb):8:in `soft_punch'"
106
+ => ["A", "B", "C", "D", ...]
107
+ >> hard_punch
108
+ "[\"AAA\", \"BBB\", \"CCC\", \"DDD\", ...] -- (irb):12:in `hard_punch'"
109
+ => ["AAA", "BBB", "CCC", "DDDD", ...]
84
110
  ```
85
111
 
86
112
  ### Registering custom punches
@@ -110,35 +136,21 @@ end
110
136
  ```
111
137
 
112
138
  ```ruby
113
- # Register the extensions
114
- DuckPuncher.register [:Billable, :Retryable]
115
-
116
139
  # Our duck
117
140
  class User < Struct.new(:name)
118
141
  end
119
142
 
143
+ # Register the extensions
144
+ DuckPuncher.register User, :Billable, :Retryable
145
+
120
146
  # Add the #punch method to User instances
121
- DuckPuncher.punch! :Object, only: :punch, target: User
147
+ DuckPuncher.punch! Object, only: :punch
122
148
 
123
149
  # Usage
124
- user = User.new('Ryan').punch(:Billable).punch(:Retryable)
150
+ user = User.new('Ryan').punch
125
151
  user.call_with_retry(19.99)
126
152
  ```
127
153
 
128
- Ducks can be registered under any name, as long as the `:mod` option specifies a module:
129
-
130
- ```ruby
131
- DuckPuncher.register :bills, mod: 'Admin::Billable'
132
- User.new.punch(:bills)
133
- ```
134
-
135
- When punching at a class level, the `:class` option is required:
136
-
137
- ```ruby
138
- DuckPuncher.register :Billable, class: 'User'
139
- DuckPuncher.punch! :Billable
140
- ```
141
-
142
154
  ## Install
143
155
 
144
156
  ```ruby
@@ -150,7 +162,7 @@ gem 'duck_puncher'
150
162
  Get notified of all punches/extensions by changing the logger level:
151
163
 
152
164
  ```ruby
153
- DuckPuncher.log.level = Logger::INFO
165
+ DuckPuncher.logger.level = Logger::INFO
154
166
  ```
155
167
 
156
168
  The default log level is `DEBUG`
@@ -169,7 +181,7 @@ LoadError: cannot load such file -- pry
169
181
  from (irb):1:in `require'
170
182
  from (irb):1
171
183
  from bin/console:10:in `<main>'
172
- >> DuckPuncher.punch! :Object, only: :require!
184
+ >> DuckPuncher.punch! Object, only: :require!
173
185
  => nil
174
186
  >> require! 'pry'
175
187
  Fetching: method_source-0.8.2.gem (100%)
data/Rakefile CHANGED
@@ -2,12 +2,12 @@ require 'bundler/gem_tasks'
2
2
  require 'rake'
3
3
  require 'rake/testtask'
4
4
 
5
- Rake::TestTask.new(:soft_punch_test) do |t|
6
- t.pattern = 'test/soft_punch/*_test.rb'
7
- end
5
+ # Rake::TestTask.new(:soft_punch_test) do |t|
6
+ # t.pattern = 'test/soft_punch/*_test.rb'
7
+ # end
8
8
 
9
9
  Rake::TestTask.new(:hard_punch_test) do |t|
10
10
  t.pattern = 'test/lib/**/*_test.rb'
11
11
  end
12
12
 
13
- task default: [:soft_punch_test, :hard_punch_test]
13
+ task default: [:hard_punch_test]
@@ -0,0 +1,20 @@
1
+ module DuckPuncher
2
+ module Decoration
3
+ def decorators
4
+ @decorators ||= ancestral_hash
5
+ end
6
+
7
+ def new_decorator(*ducks)
8
+ targets = ducks.sort.map(&:target)
9
+ decorator_class = DelegateClass(targets.first)
10
+ DuckPuncher.redefine_constant "#{targets.first.to_s.tr(':', '')}Delegator", decorator_class
11
+ ducks.each { |duck| duck.punch target: decorator_class, method: :prepend }
12
+ decorator_class
13
+ end
14
+
15
+ def undecorate(obj)
16
+ obj = obj.__getobj__ while obj.respond_to? :__getobj__
17
+ obj
18
+ end
19
+ end
20
+ end
@@ -1,17 +1,17 @@
1
1
  module DuckPuncher
2
2
  class Duck
3
- attr_accessor :name, :options
3
+ attr_accessor :target, :mod, :options
4
4
 
5
- # @param [Symbol] name of the duck
5
+ # @todo test punching a module
6
+ # @param target [String,Class] Class or module to punch
7
+ # @param mod [String,Module] The module that defines the extensions (@name is used by default)
6
8
  # @param [Hash] options to modify the duck #punch method behavior
7
- # @option options [String,Module] :mod The module that defines the extensions (@name is used by default)
8
- # @option options [String,Class] :class (name) to punch
9
- # @option options [Proc] :if Stops +punch+ if it returns false
10
- # @option options [Proc] :before A hook that is called with the target class before +punch+
11
- # @option options [Proc] :after A hook that is called with the target class after +punch+
12
- def initialize(name, options = {})
13
- @name = name
9
+ # @option options :before [Proc] A hook that is called with the target class before +punch+
10
+ # @option options :after [Proc] A hook that is called with the target class after +punch+
11
+ def initialize(target, mod, options = {})
14
12
  @options = options
13
+ @target = DuckPuncher.lookup_constant(target)
14
+ @mod = DuckPuncher.lookup_constant(mod)
15
15
  end
16
16
 
17
17
  # @param [Hash] opts to modify punch
@@ -20,11 +20,7 @@ module DuckPuncher
20
20
  # @option options [Symbol,String] :method Specifies if the methods should be included or prepended (:include)
21
21
  # @return [Class] The class that was just punched
22
22
  def punch(opts = {})
23
- if options[:if] && !options[:if].call
24
- DuckPuncher.log.info %Q(Skipping the punch for #{name}!)
25
- return nil
26
- end
27
- target = opts.delete(:target) || lookup_class
23
+ target = opts.delete(:target) || self.target
28
24
  Array(target).each do |klass|
29
25
  options[:before].call(klass) if options[:before]
30
26
  klass.extend Usable
@@ -34,42 +30,24 @@ module DuckPuncher
34
30
  target
35
31
  end
36
32
 
37
- def mod
38
- if options[:mod]
39
- lookup_constant(options[:mod])
40
- else
41
- DuckPuncher::Ducks.const_get(name)
42
- end
43
- end
44
-
45
- # @return [Class] The class that is given to initialize as the option :class or the name of the current duck (module extension)
46
- def lookup_class
47
- lookup_constant(options[:class] || name)
48
- end
49
-
50
- def lookup_constant(const)
51
- return const if Module === const
52
- if const.to_s.respond_to?(:constantize)
53
- const.to_s.constantize
54
- else
55
- const.to_s.split('::').inject(Object) { |k, part| k.const_get(part) }
56
- end
57
- end
58
-
59
- def classify
60
- Class.new(lookup_class).tap { |k| punch target: k }
61
- end
62
-
63
33
  #
64
34
  # Required to play nice in a Set
65
35
  #
66
36
 
67
37
  def eql?(other)
68
- name == other.name
38
+ "#{target}-#{mod}" == "#{other.target}-#{other.mod}"
69
39
  end
70
40
 
71
41
  def hash
72
- name.hash
42
+ target.to_s.hash + mod.to_s.hash
43
+ end
44
+
45
+ #
46
+ # Required for sorting
47
+ #
48
+
49
+ def <=>(other)
50
+ target <=> other.target
73
51
  end
74
52
  end
75
53
  end
@@ -17,6 +17,10 @@ module DuckPuncher
17
17
 
18
18
  sought_value
19
19
  end unless method_defined?(:dig)
20
+
21
+ def compact
22
+ delete_if { |_, v| v.nil? }
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -0,0 +1,13 @@
1
+ module DuckPuncher
2
+ module Ducks
3
+ module Module
4
+ def local_methods
5
+ instance_methods(false).concat constants(false)
6
+ .map! { |c| const_get(c) }
7
+ .keep_if { |c| c.respond_to?(:instance_methods) }
8
+ .flat_map { |c| c.instance_methods(false) }
9
+ .uniq
10
+ end
11
+ end
12
+ end
13
+ end
@@ -11,17 +11,30 @@ module DuckPuncher
11
11
  end
12
12
  end
13
13
 
14
- # @description Adds the duck punches to the curren object (meant to be used on instances)
15
- def punch(duck_name = self.class.name)
16
- extend Ducks[duck_name.to_sym].mod
14
+ # @description Returns a new decorated version of ourself with the punches mixed in
15
+ # @return [<self.class>Delegator]
16
+ def punch
17
+ DuckPuncher.decorators[DuckPuncher.undecorate(self).class].new(self)
18
+ end
19
+
20
+ # @description Adds the duck punches to the current object (meant to be used on instances, careful with nil and numbers!)
21
+ # @return self
22
+ def punch!
23
+ DuckPuncher::Ducks.load_mods(DuckPuncher.undecorate(self).class).each { |mod| self.extend mod }
24
+ self
25
+ end
26
+
27
+ def echo
28
+ p "#{self} -- #{caller_locations[respond_to?(:__getobj__) ? 2 : 0]}"
29
+ self
17
30
  end
18
31
 
19
32
  def track
20
33
  begin
21
- require 'object_tracker'
34
+ require 'object_tracker' || raise(LoadError)
22
35
  rescue LoadError
23
- DuckPuncher.punch! :Object, only: :require! unless respond_to? :require!
24
- require!('object_tracker')
36
+ DuckPuncher.punch! Object, only: :require! unless respond_to? :require!
37
+ require! 'object_tracker'
25
38
  end
26
39
  extend ::ObjectTracker
27
40
  track_all!
@@ -1,34 +1,30 @@
1
1
  module DuckPuncher
2
2
  module Ducks
3
+ autoload :String, 'duck_puncher/ducks/string'
4
+ autoload :Array, 'duck_puncher/ducks/array'
5
+ autoload :Numeric, 'duck_puncher/ducks/numeric'
6
+ autoload :Hash, 'duck_puncher/ducks/hash'
7
+ autoload :Object, 'duck_puncher/ducks/object'
8
+ autoload :Method, 'duck_puncher/ducks/method'
9
+ autoload :ActiveRecord, 'duck_puncher/ducks/active_record'
10
+ autoload :Module, 'duck_puncher/ducks/module'
11
+
3
12
  class << self
4
13
  def list
5
- @list ||= Set.new [
6
- Duck.new(:String),
7
- Duck.new(:Array),
8
- Duck.new(:Numeric),
9
- Duck.new(:Hash),
10
- Duck.new(:Object),
11
- Duck.new(:Method, before: ->(*) { DuckPuncher::GemInstaller.initialize! }),
12
- Duck.new(:ActiveRecord, class: 'ActiveRecord::Base', if: -> { defined? ::ActiveRecord })
13
- ]
14
+ @list ||= DuckPuncher.ancestral_hash
14
15
  end
15
16
 
16
- def [](name)
17
- list.find { |duck| duck.name == name.to_sym } ||
18
- fail(ArgumentError, %Q(Couldn't find "#{name}" in my list of Ducks! I know about: #{list.map(&:name).map(&:to_s)}))
17
+ def [](klass)
18
+ list[klass]
19
19
  end
20
20
 
21
- def load_path_for(duck)
22
- "duck_puncher/ducks/#{duck.name.to_s.gsub(/\B([A-Z])/, '_\1').downcase}"
21
+ def load_mods(klass, loaded_mods: [])
22
+ if klass.respond_to?(:superclass)
23
+ load_mods(klass.superclass, loaded_mods: list[klass].to_a.map(&:mod) + loaded_mods)
24
+ else
25
+ loaded_mods
26
+ end
23
27
  end
24
28
  end
25
-
26
- #
27
- # Autoload our ducks
28
- #
29
-
30
- list.each do |duck|
31
- autoload duck.name, load_path_for(duck)
32
- end
33
29
  end
34
30
  end
@@ -0,0 +1,21 @@
1
+ module DuckPuncher
2
+ module Registration
3
+ def register(target, *mods)
4
+ options = mods.last.is_a?(Hash) ? mods.pop : {}
5
+ target = DuckPuncher.lookup_constant target
6
+ Ducks.list[target] = [] unless Ducks.list.key?(target)
7
+ Array(mods).each do |mod|
8
+ duck = Duck.new target, mod, options
9
+ Ducks.list[target] << duck
10
+ decorators[target] = new_decorator(duck, *Ducks[target])
11
+ end
12
+ nil
13
+ end
14
+
15
+ def deregister(*classes)
16
+ classes.each &Ducks.list.method(:delete)
17
+ classes.each &decorators.method(:delete)
18
+ nil
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module DuckPuncher
2
- VERSION = '3.0.0'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
data/lib/duck_puncher.rb CHANGED
@@ -1,8 +1,17 @@
1
+ # Standard lib
1
2
  require 'pathname'
2
3
  require 'fileutils'
3
4
  require 'logger'
5
+ require 'set'
6
+ require 'delegate'
7
+
8
+ # Gems
4
9
  require 'usable'
10
+
11
+ # Our stuff
5
12
  require 'duck_puncher/version'
13
+ require 'duck_puncher/registration'
14
+ require 'duck_puncher/decoration'
6
15
 
7
16
  module DuckPuncher
8
17
  autoload :JSONStorage, 'duck_puncher/json_storage'
@@ -11,48 +20,52 @@ module DuckPuncher
11
20
  autoload :Ducks, 'duck_puncher/ducks'
12
21
 
13
22
  class << self
23
+ include Registration, Decoration
24
+
14
25
  attr_accessor :log
26
+ alias_method :logger, :log
15
27
 
16
- def classes
17
- @classes ||= {}
28
+ def punch!(*classes)
29
+ options = classes.last.is_a?(Hash) ? classes.pop : {}
30
+ classes.each do |klass|
31
+ klass = lookup_constant(klass)
32
+ Ducks[klass].sort.each do |duck|
33
+ punches = options[:only] || Ducks::Module.instance_method(:local_methods).bind(duck.mod).call
34
+ log.info %Q(#{duck.target}#{" <-- #{punches}" if Array(punches).any?})
35
+ options[:target] = klass
36
+ unless duck.punch(options)
37
+ log.error %Q(Failed to punch #{name})
38
+ end
39
+ end
40
+ end
41
+ nil
18
42
  end
19
43
 
20
- def duck_class(name)
21
- classes[name] ||= const_set "#{name}Duck", Ducks[name].dup.classify
44
+ def punch_all!
45
+ punch! *Ducks.list.keys
22
46
  end
23
47
 
24
- # @description Extends functionality to a copy of the specified class
25
- def punch(*names)
26
- singular = names.size == 1
27
- punched_ducks = names.map(&method(:duck_class)).compact
28
- if singular
29
- punched_ducks.first
48
+ def lookup_constant(const)
49
+ return const if Module === const
50
+ if const.to_s.respond_to?(:constantize)
51
+ const.to_s.constantize
30
52
  else
31
- punched_ducks
32
- end
33
- end
34
-
35
- def punch!(*names)
36
- options = names.last.is_a?(Hash) ? names.pop : {}
37
- names.each do |name|
38
- duck = Ducks[name]
39
- log.warn %Q(Punching#{" #{options[:only]} onto" if Array(options[:only]).any?} #{options.fetch(:target, name)})
40
- unless duck.punch(options)
41
- log.error %Q(Failed to punch #{name}!)
42
- end
53
+ const.to_s.split('::').inject(Object) { |k, part| k.const_get(part) }
43
54
  end
55
+ rescue NameError => e
56
+ log.error "#{e.class}: #{e.message}"
44
57
  nil
45
58
  end
46
59
 
47
- def punch_all!
48
- log.warn 'Punching all ducks!'
49
- Ducks.list.each &:punch
60
+ def redefine_constant(name, const)
61
+ if const_defined? name
62
+ remove_const name
63
+ end
64
+ const_set name, const
50
65
  end
51
66
 
52
- def register(*args)
53
- Array(args.shift).each do |name|
54
- Ducks.list << Duck.new(name, *args)
55
- end
67
+ def ancestral_hash
68
+ Hash.new { |me, klass| me[klass.superclass] if klass.respond_to?(:superclass) }
56
69
  end
57
70
  end
58
71
 
@@ -62,4 +75,18 @@ module DuckPuncher
62
75
  end
63
76
 
64
77
  log.level = Logger::ERROR
78
+
79
+ ducks = [
80
+ [String, Ducks::String],
81
+ [Array, Ducks::Array],
82
+ [Numeric, Ducks::Numeric],
83
+ [Hash, Ducks::Hash],
84
+ [Object, Ducks::Object],
85
+ [Module, Ducks::Module],
86
+ [Method, Ducks::Method, { before: ->(*) { DuckPuncher::GemInstaller.initialize! } }],
87
+ ]
88
+ ducks << ['ActiveRecord::Base', Ducks::ActiveRecord] if defined? ::ActiveRecord
89
+ ducks.each do |duck|
90
+ register *duck
91
+ end
65
92
  end
@@ -1,15 +1,16 @@
1
1
  require_relative '../../test_helper'
2
2
 
3
- DuckPuncher.punch! :Object
3
+ DuckPuncher.punch! Object
4
4
 
5
5
  class ArrayTest < MiniTest::Test
6
6
  attr_reader :subject
7
7
 
8
8
  def setup
9
- @subject = ('a'..'m').to_a.punch
9
+ @subject = ('a'..'m').to_a
10
10
  end
11
11
 
12
12
  def test_m
13
+ subject.punch!
13
14
  assert_equal subject.map(&:upcase), subject.m(:upcase)
14
15
  refute_equal subject.object_id, subject.m(:upcase).object_id
15
16
  assert_equal subject.map!(&:upcase), subject.m!(:upcase)
@@ -17,6 +18,7 @@ class ArrayTest < MiniTest::Test
17
18
  end
18
19
 
19
20
  def test_mm_with_two_args
21
+ subject.punch!
20
22
  assert_equal subject.map { |x| x.prepend('btn-') }, subject.mm(:prepend, 'btn-')
21
23
  refute_equal subject.object_id, subject.mm(:prepend, 'btn-')
22
24
  assert_equal subject.map! { |x| x.prepend('btn-') }, subject.mm!(:prepend, 'btn-')
@@ -24,10 +26,12 @@ class ArrayTest < MiniTest::Test
24
26
  end
25
27
 
26
28
  def test_mm_with_three_args
29
+ @subject = @subject.punch
27
30
  assert_equal subject.map { |x| x.sub(/[aeiou]/, '*') }, subject.mm(:sub, /[aeiou]/, '*')
28
31
  end
29
32
 
30
33
  def test_except
34
+ @subject = @subject.punch
31
35
  assert_equal subject.except('a'), %w[b c d e f g h i j k l m]
32
36
  assert_equal subject.except('a', 'b', 'c'), %w[d e f g h i j k l m]
33
37
  assert_equal subject.except, subject
@@ -1,10 +1,10 @@
1
1
  require_relative '../../test_helper'
2
2
 
3
- DuckPuncher.punch :Hash
3
+ DuckPuncher.punch! Hash
4
4
 
5
5
  class HashTest < MiniTest::Test
6
6
  def setup
7
- @subject = DuckPuncher::HashDuck.new.merge({ a: 1, b: { c: 2 } })
7
+ @subject = { a: 1, b: { c: 2 } }
8
8
  end
9
9
 
10
10
  def test_dig
@@ -13,4 +13,11 @@ class HashTest < MiniTest::Test
13
13
  assert_equal @subject.dig(:b, :c), 2
14
14
  assert_equal @subject.dig(:b), { c: 2 }
15
15
  end
16
+
17
+ def test_compact
18
+ assert_equal @subject.compact, { a: 1, b: { c: 2 } }
19
+ @subject[:b] = nil
20
+ assert_equal @subject, { a: 1, b: nil }
21
+ assert_equal @subject.compact, { a: 1 }
22
+ end
16
23
  end
@@ -1,6 +1,6 @@
1
1
  require_relative '../../test_helper'
2
2
  require_relative '../../fixtures/wut'
3
- DuckPuncher.punch! :Method
3
+ DuckPuncher.punch! Method
4
4
 
5
5
  class MethodTest < MiniTest::Test
6
6
 
@@ -0,0 +1,9 @@
1
+ require_relative '../../test_helper'
2
+
3
+ DuckPuncher.punch! Module
4
+
5
+ class ModuleTest < MiniTest::Test
6
+ def test_local_methods
7
+ assert_equal ModWithNestedMod.local_methods, [:instance_method_1, :class_method_1]
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  require_relative '../../test_helper'
2
- DuckPuncher.punch! :Numeric
2
+ DuckPuncher.punch! Numeric
3
3
 
4
4
  class NumericTest < MiniTest::Test
5
5
 
@@ -1,57 +1,60 @@
1
1
  require_relative '../../test_helper'
2
- DuckPuncher.punch! :Object
2
+ DuckPuncher.punch! Object
3
3
 
4
4
  class ObjectTest < MiniTest::Test
5
-
6
5
  def setup
7
- Object.const_set :User, Class.new
8
- @subject = Object.new
9
- @user = User.new
6
+ @animal = Animal.new
7
+ @dog = Dog.new
8
+ @kaia = Kaia.new
10
9
  end
11
10
 
12
11
  def teardown
13
- DuckPuncher::Ducks.list.delete_if { |duck| [:admin, :super_admin, :User].include?(duck.name) }
14
- Object.send :remove_const, :User
12
+ DuckPuncher.deregister Animal, Dog, Kaia
15
13
  end
16
14
 
17
15
  def test_clone!
18
- cloned = @subject.clone!
19
- assert_equal cloned.class, @subject.class
20
- refute_equal cloned, @subject
21
- end
22
-
23
- def test_punch_on_a_core_duck
24
- refute [].respond_to?(:m)
25
- assert [].respond_to?(:punch)
26
- assert [].punch.respond_to?(:m)
16
+ cloned = @dog.clone!
17
+ assert_equal cloned.class, @dog.class
18
+ refute_equal cloned, @dog
27
19
  end
28
20
 
29
21
  def test_punch_with_a_core_duck
30
- assert [].punch(:Array).respond_to?(:m)
22
+ assert [].punch.respond_to?(:m)
31
23
  end
32
24
 
33
25
  def test_punch_on_a_custom_duck
34
- DuckPuncher.register :User, mod: 'CustomPunch2'
35
- assert @user.punch.respond_to?(:quack)
26
+ DuckPuncher.register Animal, CustomPunch2
27
+ assert @animal.punch.respond_to?(:quack)
36
28
  end
37
29
 
38
- def test_punch_with_a_custom_duck
39
- refute @user.respond_to?(:quack)
40
- DuckPuncher.register :admin, mod: 'CustomPunch2'
41
- assert @user.punch(:admin).respond_to?(:quack)
42
-
43
- refute @user.respond_to?(:wobble)
44
- DuckPuncher.register :super_admin, mod: 'CustomPunch3'
45
- assert @user.punch(:super_admin).respond_to?(:wobble)
30
+ def test_punch_with_multiple_custom_duck
31
+ DuckPuncher.register Animal, CustomPunch2
32
+ DuckPuncher.register Animal, CustomPunch3
33
+ assert @animal.punch.respond_to?(:wobble)
46
34
  end
47
35
 
48
36
  def test_punch_call_stack
49
- User.send(:define_method, :foo) { quack }
50
- User.send(:define_method, :quack) { 'foo' }
51
- assert_equal 'foo', @user.foo
52
- DuckPuncher.register :User, mod: 'CustomPunch2'
53
- assert_equal 'quack', @user.punch.foo
54
- User.send(:remove_method, :foo)
55
- User.send(:remove_method, :quack)
37
+ Animal.send(:define_method, :foo) { quack }
38
+ Animal.send(:define_method, :quack) { 'foo' }
39
+ assert_equal 'foo', @animal.foo
40
+ DuckPuncher.register Animal, CustomPunch2
41
+ @animal.punch!
42
+ assert_equal 'quack', @animal.foo
43
+ end
44
+
45
+ def test_punch_on_ancestor_only
46
+ DuckPuncher.register Dog, CustomPunch2
47
+ assert_respond_to @dog.punch, :quack
48
+ end
49
+
50
+ def test_punch_includes_all_ancestors
51
+ DuckPuncher.register Animal, CustomPunch2
52
+ DuckPuncher.register Dog, CustomPunch
53
+ DuckPuncher.register Kaia, CustomPunch3
54
+ @kaia = Kaia.new
55
+ @kaia.punch!
56
+ assert_respond_to @kaia, :wobble
57
+ assert_respond_to @kaia, :talk
58
+ assert_respond_to @kaia, :quack
56
59
  end
57
60
  end
@@ -1,6 +1,6 @@
1
1
  require_relative '../../test_helper'
2
2
 
3
- DuckPuncher.punch! :String
3
+ DuckPuncher.punch! String
4
4
 
5
5
  class StringTest < MiniTest::Test
6
6
  def test_pluralize
@@ -1,27 +1,55 @@
1
1
  require_relative '../test_helper'
2
2
 
3
+ DuckPuncher.punch! Object, only: :punch
4
+
3
5
  class DuckPuncherTest < MiniTest::Test
4
- def test_register
5
- refute_respond_to [], :tap_tap
6
- DuckPuncher.register :CustomPunch, class: 'Array'
7
- DuckPuncher.punch! :CustomPunch
8
- assert_respond_to [], :tap_tap
9
- DuckPuncher.punch! :Object, only: :punch
10
- assert_respond_to [].punch(:CustomPunch), :tap_tap
11
- # does not re-register duck with same name
12
- duck_list_size = DuckPuncher::Ducks.list.size
13
- DuckPuncher.register :CustomPunch, class: 'String'
14
- assert_equal duck_list_size, DuckPuncher::Ducks.list.size
6
+ def setup
7
+ @subject = Animal.new
8
+ @kaia = Kaia.new
9
+ DuckPuncher.deregister Animal, Kaia, Dog
10
+ end
11
+
12
+ def teardown
13
+ DuckPuncher.deregister Animal, Kaia, Dog
14
+ end
15
+
16
+ def test_punch!
17
+ refute_respond_to @kaia, :talk
18
+ refute_respond_to @kaia.punch, :talk
19
+ DuckPuncher.register Kaia, CustomPunch
20
+ DuckPuncher.punch! Kaia, only: :talk
21
+ assert_respond_to @kaia, :talk
22
+ assert_respond_to @kaia.punch, :talk
23
+ end
24
+
25
+
26
+ def test_punch_all!
27
+ DuckPuncher.punch_all!
28
+ expected_methods = DuckPuncher::Ducks.list.values.m(:to_a).flatten.m(:mod).m(:local_methods).flatten
29
+ assert expected_methods.size > 1
30
+ good_ducks = DuckPuncher::Ducks.list.select { |_, ducks|
31
+ ducks.all? { |duck| (duck.mod.local_methods - duck.target.instance_methods(:false)).size.zero? }
32
+ }
33
+ assert good_ducks.size > 5
34
+ end
35
+
36
+ def test_register_with_multiple_mods
37
+ refute_respond_to @subject, :talk
38
+ refute_respond_to @subject, :wobble
39
+ refute_respond_to @subject.punch, :talk
40
+ refute_respond_to @subject.punch, :wobble
41
+ DuckPuncher.register Animal, CustomPunch, CustomPunch3
42
+ assert_respond_to @subject.punch, :talk
43
+ assert_respond_to @subject.punch, :wobble
15
44
  end
16
45
 
17
- def test_register_with_array
18
- refute_respond_to [], :quack
19
- refute_respond_to [], :wobble
20
- DuckPuncher.register [:CustomPunch2, :CustomPunch3], class: 'Array'
21
- DuckPuncher.punch! :CustomPunch2
22
- assert_respond_to [], :quack
23
- refute_respond_to [], :wobble
24
- DuckPuncher.punch! :CustomPunch3
25
- assert_respond_to [], :wobble
46
+ def test_deregister
47
+ refute_respond_to @subject, :talk
48
+ refute_respond_to @subject.punch, :talk
49
+ DuckPuncher.register Animal, CustomPunch
50
+ assert_respond_to @subject.punch, :talk
51
+ refute_respond_to @subject, :talk
52
+ DuckPuncher.deregister Animal
53
+ refute_respond_to @subject.punch, :talk
26
54
  end
27
55
  end
data/test/test_helper.rb CHANGED
@@ -8,7 +8,7 @@ Minitest::Reporters.use!
8
8
  DuckPuncher.log.level = Logger::INFO
9
9
 
10
10
  module CustomPunch
11
- def tap_tap
11
+ def talk
12
12
  p self
13
13
  self
14
14
  end
@@ -24,3 +24,22 @@ module CustomPunch3
24
24
  def wobble
25
25
  end
26
26
  end
27
+
28
+ module ModWithNestedMod
29
+ def instance_method_1
30
+ end
31
+
32
+ module ClassMethods
33
+ def class_method_1
34
+ end
35
+ end
36
+ end
37
+
38
+ class Animal
39
+ end
40
+
41
+ class Dog < Animal
42
+ end
43
+
44
+ class Kaia < Dog
45
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duck_puncher
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Buckley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-05 00:00:00.000000000 Z
11
+ date: 2016-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: usable
@@ -105,27 +105,30 @@ files:
105
105
  - bin/console
106
106
  - duck_puncher.gemspec
107
107
  - lib/duck_puncher.rb
108
+ - lib/duck_puncher/decoration.rb
108
109
  - lib/duck_puncher/duck.rb
109
110
  - lib/duck_puncher/ducks.rb
110
111
  - lib/duck_puncher/ducks/active_record.rb
111
112
  - lib/duck_puncher/ducks/array.rb
112
113
  - lib/duck_puncher/ducks/hash.rb
113
114
  - lib/duck_puncher/ducks/method.rb
115
+ - lib/duck_puncher/ducks/module.rb
114
116
  - lib/duck_puncher/ducks/numeric.rb
115
117
  - lib/duck_puncher/ducks/object.rb
116
118
  - lib/duck_puncher/ducks/string.rb
117
119
  - lib/duck_puncher/gem_installer.rb
118
120
  - lib/duck_puncher/json_storage.rb
121
+ - lib/duck_puncher/registration.rb
119
122
  - lib/duck_puncher/version.rb
120
123
  - test/fixtures/wut.rb
121
124
  - test/lib/duck_puncher/array_test.rb
122
125
  - test/lib/duck_puncher/hash_test.rb
123
126
  - test/lib/duck_puncher/method_test.rb
127
+ - test/lib/duck_puncher/module_test.rb
124
128
  - test/lib/duck_puncher/numeric_test.rb
125
129
  - test/lib/duck_puncher/object_test.rb
126
130
  - test/lib/duck_puncher/string_test.rb
127
131
  - test/lib/duck_puncher_test.rb
128
- - test/soft_punch/duck_puncher_test.rb
129
132
  - test/test_helper.rb
130
133
  homepage: https://github.com/ridiculous/duck_puncher
131
134
  licenses:
@@ -156,9 +159,9 @@ test_files:
156
159
  - test/lib/duck_puncher/array_test.rb
157
160
  - test/lib/duck_puncher/hash_test.rb
158
161
  - test/lib/duck_puncher/method_test.rb
162
+ - test/lib/duck_puncher/module_test.rb
159
163
  - test/lib/duck_puncher/numeric_test.rb
160
164
  - test/lib/duck_puncher/object_test.rb
161
165
  - test/lib/duck_puncher/string_test.rb
162
166
  - test/lib/duck_puncher_test.rb
163
- - test/soft_punch/duck_puncher_test.rb
164
167
  - test/test_helper.rb
@@ -1,30 +0,0 @@
1
- require_relative '../test_helper'
2
-
3
- class DuckPuncherTest < MiniTest::Test
4
- DuckString = DuckPuncher.punch :String
5
- DuckNumber, DuckArray = DuckPuncher.punch :Numeric, :Array
6
-
7
- def test_duck_string
8
- refute_respond_to '', :underscore
9
- assert_respond_to DuckString.new, :underscore
10
- refute_respond_to '', :underscore
11
- end
12
-
13
- def test_duck_number_array
14
- refute_respond_to 25, :to_currency
15
- refute_respond_to [], :m
16
- assert_respond_to DuckNumber.new, :to_currency
17
- assert_respond_to DuckArray.new, :m
18
- refute_respond_to 25, :to_currency
19
- refute_respond_to [], :m
20
- end
21
-
22
- def test_excluding_punches
23
- refute_respond_to Object.new, :punch
24
- DuckPuncher.punch! :Object, only: :punch
25
- assert_respond_to Object.new, :punch
26
- refute_respond_to Object.new, :require!
27
- DuckPuncher.punch! :Object, only: :require!
28
- assert_respond_to Object.new, :require!
29
- end
30
- end