duck_puncher 3.0.0 → 4.0.0

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
  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