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 +4 -4
- data/README.md +70 -58
- data/Rakefile +4 -4
- data/lib/duck_puncher/decoration.rb +20 -0
- data/lib/duck_puncher/duck.rb +20 -42
- data/lib/duck_puncher/ducks/hash.rb +4 -0
- data/lib/duck_puncher/ducks/module.rb +13 -0
- data/lib/duck_puncher/ducks/object.rb +19 -6
- data/lib/duck_puncher/ducks.rb +18 -22
- data/lib/duck_puncher/registration.rb +21 -0
- data/lib/duck_puncher/version.rb +1 -1
- data/lib/duck_puncher.rb +56 -29
- data/test/lib/duck_puncher/array_test.rb +6 -2
- data/test/lib/duck_puncher/hash_test.rb +9 -2
- data/test/lib/duck_puncher/method_test.rb +1 -1
- data/test/lib/duck_puncher/module_test.rb +9 -0
- data/test/lib/duck_puncher/numeric_test.rb +1 -1
- data/test/lib/duck_puncher/object_test.rb +37 -34
- data/test/lib/duck_puncher/string_test.rb +1 -1
- data/test/lib/duck_puncher_test.rb +48 -20
- data/test/test_helper.rb +20 -1
- metadata +7 -4
- data/test/soft_punch/duck_puncher_test.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f945aea9eb0db7392e51aea5cda254eaad6d5815
|
4
|
+
data.tar.gz: fa1e7a2565caee23e437d38b60358f9fbf382901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
6
|
+
* as a decorator
|
7
7
|
|
8
8
|
Default extensions:
|
9
9
|
|
10
10
|
```ruby
|
11
|
-
Array #m
|
12
|
-
#m!
|
13
|
-
#mm
|
14
|
-
#mm!
|
15
|
-
#except
|
16
|
-
Hash #dig
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
#
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
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!
|
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!
|
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!
|
61
|
+
DuckPuncher.punch! Numeric, only: [:to_currency, :to_duration]
|
57
62
|
```
|
58
63
|
|
59
|
-
|
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 :
|
63
|
-
|
64
|
-
|
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
|
-
|
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!
|
74
|
-
>> %w[
|
75
|
-
=> [
|
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
|
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
|
82
|
-
>> %w[ducks]
|
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!
|
147
|
+
DuckPuncher.punch! Object, only: :punch
|
122
148
|
|
123
149
|
# Usage
|
124
|
-
user = User.new('Ryan').punch
|
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.
|
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!
|
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
|
-
|
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: [:
|
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
|
data/lib/duck_puncher/duck.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
module DuckPuncher
|
2
2
|
class Duck
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :target, :mod, :options
|
4
4
|
|
5
|
-
# @
|
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 [
|
8
|
-
# @option options [
|
9
|
-
|
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
|
-
|
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
|
-
|
38
|
+
"#{target}-#{mod}" == "#{other.target}-#{other.mod}"
|
69
39
|
end
|
70
40
|
|
71
41
|
def hash
|
72
|
-
|
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
|
@@ -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
|
15
|
-
|
16
|
-
|
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!
|
24
|
-
require!
|
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!
|
data/lib/duck_puncher/ducks.rb
CHANGED
@@ -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 ||=
|
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 [](
|
17
|
-
list
|
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
|
22
|
-
|
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
|
data/lib/duck_puncher/version.rb
CHANGED
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
|
-
|
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
|
21
|
-
|
44
|
+
def punch_all!
|
45
|
+
punch! *Ducks.list.keys
|
22
46
|
end
|
23
47
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
48
|
-
|
49
|
-
|
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
|
53
|
-
|
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!
|
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
|
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
|
3
|
+
DuckPuncher.punch! Hash
|
4
4
|
|
5
5
|
class HashTest < MiniTest::Test
|
6
6
|
def setup
|
7
|
-
@subject =
|
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,57 +1,60 @@
|
|
1
1
|
require_relative '../../test_helper'
|
2
|
-
DuckPuncher.punch!
|
2
|
+
DuckPuncher.punch! Object
|
3
3
|
|
4
4
|
class ObjectTest < MiniTest::Test
|
5
|
-
|
6
5
|
def setup
|
7
|
-
|
8
|
-
@
|
9
|
-
@
|
6
|
+
@animal = Animal.new
|
7
|
+
@dog = Dog.new
|
8
|
+
@kaia = Kaia.new
|
10
9
|
end
|
11
10
|
|
12
11
|
def teardown
|
13
|
-
DuckPuncher
|
14
|
-
Object.send :remove_const, :User
|
12
|
+
DuckPuncher.deregister Animal, Dog, Kaia
|
15
13
|
end
|
16
14
|
|
17
15
|
def test_clone!
|
18
|
-
cloned = @
|
19
|
-
assert_equal cloned.class, @
|
20
|
-
refute_equal cloned, @
|
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
|
22
|
+
assert [].punch.respond_to?(:m)
|
31
23
|
end
|
32
24
|
|
33
25
|
def test_punch_on_a_custom_duck
|
34
|
-
DuckPuncher.register
|
35
|
-
assert @
|
26
|
+
DuckPuncher.register Animal, CustomPunch2
|
27
|
+
assert @animal.punch.respond_to?(:quack)
|
36
28
|
end
|
37
29
|
|
38
|
-
def
|
39
|
-
|
40
|
-
DuckPuncher.register
|
41
|
-
assert @
|
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
|
-
|
50
|
-
|
51
|
-
assert_equal 'foo', @
|
52
|
-
DuckPuncher.register
|
53
|
-
|
54
|
-
|
55
|
-
|
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,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
|
5
|
-
|
6
|
-
|
7
|
-
DuckPuncher.
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
18
|
-
refute_respond_to
|
19
|
-
refute_respond_to
|
20
|
-
DuckPuncher.register
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
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:
|
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-
|
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
|