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