DuckPuncher
DuckPuncher provides an interface for administering duck punches (a.k.a "monkey patches"). Punches can be administered in several ways:
- as an extension
- as a decorator
Default extensions:
Enumerable
#m => `[].m(:to_s)` => `[].map(&:to_s)`
#m! => `[].m!(:upcase)` => `[].map!(&:upcase)`
#mm => `[].mm(:sub, /[aeiou]/, '*')` => `[].map { |x| x.sub(/[aeiou]/, '*') }`
#mm! => `[].mm!(:sub, /[aeiou]/, '*')` => `[].map! { |x| x.sub(/[aeiou]/, '*') }`
#except => `[].except('foo', 'bar')` => `[] - ['foo', 'bar']`
#map_keys => `[{id: 1, name: 'foo'}, {id: 2}].map_keys(:id)` => `[1, 2]`
Hash
#dig => `{a: 1, b: {c: 2}}.dig(:b, :c)` => 2 (Part of standard lib in Ruby >= 2.3)
#compact => `{a: 1, b: nil}.compact` => {a: 1}
Numeric
#to_currency => `25.245.to_currency` => 25.25
#to_duration => `10_000.to_duration` => '2 h 46 min'
#to_time_ago => `10_000.to_time_ago` => '2 hours ago'
#to_rad => `10.15.to_rad` => 0.17715091907742445
String
#pluralize => `'hour'.pluralize(2)` => "hours"
#underscore => `'DuckPuncher::JSONStorage'.underscore` => 'duck_puncher/json_storage'
#to_boolean => `'1'.to_boolean` => true
#constantize => `'MiniTest::Test'.constantize` => MiniTest::Test
Module
#local_methods => `Kernel.local_methods` returns the methods defined directly in the class + nested constants w/ methods
Object
#clone! => `Object.new.clone!` => a deep clone of the object (using Marshal.dump)
#punch => `'duck'.punch` => a copy of 'duck' with String punches mixed in
#punch! => `'duck'.punch!` => destructive version applies extensions directly to the base object
#echo => `'duck'.echo.upcase` => spits out the caller and value of the object and returns the object
#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)
Method
#to_instruct => `Benchmark.method(:measure).to_instruct` returns the Ruby VM instruction sequence for the method
#to_source => `Benchmark.method(:measure).to_source` returns the method definition as a string
Usage
Punch all registered ducks:
DuckPuncher.()
Punch individual ducks by name:
DuckPuncher.(Hash, Object)
One method to rule them all:
DuckPuncher.(Object, only: :punch)
Tactical punches
DuckPuncher
extends the amazing Usable gem, so you can configure only the punches you want! For instance:
DuckPuncher.(Numeric, only: [:to_currency, :to_duration])
If you punch Object
then you can use #punch!
on any object to extend individual instances:
>> DuckPuncher.(Object, only: :punch!)
>> %w[yes no 1].punch!.m!(:punch).m(:to_boolean)
=> [true, false, true]
Alternatively, there is also the Object#punch
method which returns a decorated copy of an object with punches mixed in:
>> DuckPuncher.(Object, only: :punch)
>> %w[1 2 3].punch.m(:to_i)
=> [1, 2, 3]
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:
>> LovableDuck = Module.new { def inspect() "I love #{self.first}" end }
>> DuckPuncher.register Array, LovableDuck
>> ducks = %w[ducks]
>> soft_punch = ducks.punch
=> "I love ducks"
>> soft_punch.class
=> DuckPuncher::ArrayDelegator
>> ducks.punch!.class
=> Array
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
a method defined echo
, but when we punch Object
, it means all subclasses have access to the same methods, even with soft punches.
def soft_punch
('a'..'z').punch.echo.to_a.map(&:upcase)
end
def hard_punch
('a'..'z').punch!.m!(:upcase).mm!(:*, 3).echo
end
>> soft_punch
"a..z -- (irb):8:in `soft_punch'"
=> ["A", "B", "C", "D", ...]
>> hard_punch
"[\"AAA\", \"BBB\", \"CCC\", \"DDD\", ...] -- (irb):12:in `hard_punch'"
=> ["AAA", "BBB", "CCC", "DDDD", ...]
Registering custom punches
DuckPuncher allows you to utilize the punch
interface to extend any kind of object with your own punches. Simply
call DuckPuncher.register
with the name of your module (or an array of names) and any of
these options.
# Define some extensions
module Billable
def call(amt)
puts "Attempting to bill #{name} for $#{amt}"
fail Errno::ENOENT
end
end
module Retryable
def call_with_retry(*args, retries: 3)
call *args
rescue Errno::ENOENT
puts 'retrying'
retry if (retries -= 1) > 0
end
end
# Our duck
class User < Struct.new(:name)
end
# Register the extensions
DuckPuncher.register User, :Billable, :Retryable
# Add the #punch method to User instances
DuckPuncher.(Object, only: :punch)
# Usage
user = User.new('Ryan').punch
user.call_with_retry(19.99)
To register and punch in one swoop, use DuckPuncher.register!
Install
gem 'duck_puncher'
Logging
Get notified of all punches/extensions by changing the logger level:
DuckPuncher.logger.level = Logger::INFO
The default log level is DEBUG
Experimental
Object#require! will try to require a gem, or, if it's not found, then download it! It will also keep track of any downloaded gems and load them for subsequent IRB/rails console sessions. Gems are not saved to the Gemfile.
In the wild:
>> `require 'pry'`
LoadError: cannot load such file -- pry
from (irb):1:in `require'
from (irb):1
from bin/console:10:in `<main>'
>> DuckPuncher.(Object, only: :require!)
=> nil
>> require! 'pry'
Fetching: method_source-0.8.2.gem (100%)
Fetching: slop-3.6.0.gem (100%)
Fetching: coderay-1.1.0.gem (100%)
Fetching: pry-0.10.3.gem (100%)
=> true
>> Pry.start
[1] pry(main)>
Perfect! Mostly ... although, it doesn't work well with bigger gems or those with native extensions ¯\_(ツ)_/¯
Object#track builds upon require!
to download the ObjectTracker gem,
if it's not available in the current load path, and starts tracking the current object!
Duck = Class.new
Donald = Module.new { def tap_tap() self end }
DuckPuncher.(:Object, only: :track)
Donald.track
Duck.track
>> Duck.usable Donald, only: :tap_tap
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00002)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00001)
* called "Donald.respond_to?" with to_ary, true [RUBY CORE] (0.00001)
* called "Donald.to_s" [RUBY CORE] (0.00001)
* called "Duck.usable_config" [ruby-2.3.0@duck_puncher/gems/usable-1.2.0/lib/usable.rb:10] (0.00002)
* called "Duck.usable_config" [ruby-2.3.0@duck_puncher/gems/usable-1.2.0/lib/usable.rb:10] (0.00001)
* called "Donald.const_defined?" with UsableSpec [RUBY CORE] (0.00001)
* called "Donald.dup" [RUBY CORE] (0.00002)
* called "Donald.name" [RUBY CORE] (0.00000)
* called "Donald.instance_methods" [RUBY CORE] (0.00001)
* called "Duck.const_defined?" with DonaldUsed [RUBY CORE] (0.00001)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00001)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00000)
* called "Donald.respond_to?" with to_ary, true [RUBY CORE] (0.00000)
* called "Donald.to_s" [RUBY CORE] (0.00035)
* called "Duck.const_set" with DonaldUsed, #<Module:0x007fe23a261618> [RUBY CORE] (0.00002)
* called "Duck.usable_config" [ruby-2.3.0@duck_puncher/gems/usable-1.2.0/lib/usable.rb:10] (0.00000)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00000)
* called "Donald.respond_to?" with to_ary, true [RUBY CORE] (0.00001)
* called "Donald.to_s" [RUBY CORE] (0.00019)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00001)
* called "Donald.respond_to?" with to_str, true [RUBY CORE] (0.00000)
* called "Donald.respond_to?" with to_ary, true [RUBY CORE] (0.00000)
* called "Donald.to_s" [RUBY CORE] (0.00000)
* called "Duck.include" with Duck::DonaldUsed [RUBY CORE] (0.00001)
* called "Duck#send" with include, Duck::DonaldUsed [RUBY CORE] (0.00024)
* called "Duck.usable!" with #<Usable::ModExtender:0x007fe23a261ca8> [ruby-2.3.0@duck_puncher/gems/usable-1.2.0/lib/usable.rb:41] (0.00143)
* called "Donald.const_defined?" with UsableSpec [RUBY CORE] (0.00001)
* called "Duck.usable" with Donald, {:only=>:tap_tap} [ruby-2.3.0@duck_puncher/gems/usable-1.2.0/lib/usable.rb:30] (0.00189)
# ... You get the idea.
Contributing
- Fork it
- Run tests with
rake
- Start an IRB console that already has all your ducks in a row:
bin/console
- Start an IRB console without punching ducks:
PUNCH=no bin/console
- Make changes and submit a PR to https://github.com/ridiculous/duck_puncher
License
MIT