0.01
No commit activity in last 3 years
No release in over 3 years
Administer precision punches
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 5.0
~> 10.1
>= 1.3

Runtime

>= 3.3
 Project Readme

DuckPuncher Gem Version Build Status Code Climate

DuckPuncher provides an interface for administering duck punches (a.k.a "monkey patches"). Punches can be applied permanently via an extension or temporarily as a decorator. Decorator classes are generated when an extension is registered and used via Object#punch. The object is wrapped in one decorator for each of the object's ancestors (with registered punches) and behaves much like it's extended cousin, Object.punch!.

Install

gem 'duck_puncher', '~> 5.0'

Usage

Punch all registered ducks:

DuckPuncher.()

Punch individual ducks by name:

DuckPuncher.(Hash, Object)

Add punch as a proxy method to all punches:

DuckPuncher.(Object, only: :punch)

Redirect the punches registered for a class to another target:

DuckPuncher.(Object, target: String)

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.

>> DuckPuncher.(Object, only: [:punch, :punch!])
>> def soft_punch() ('a'..'z').punch.echo(1).map(&:upcase) end
>> def hard_punch() ('a'..'z').punch!.echo(1).mm(:*, 3) end
>> soft_punch
"a".."z"
* /.../delegate.rb:85:in `method_missing'
=> ["A", "B", "C", "D", "E", "F", "G", "H", "I", ...]
>> hard_punch
"a".."z"
* (irb):11:in `hard_punch'
=> ["AAA", "BBB", "CCC", "DDDD", ...]

Default extensions

Enumerable (including Array, Set, Range, and Enumerator)

[].m(:to_s)
[].m!(:upcase)
[].mm(:sub, /[aeiou]/, '*')
[].mm!(:sub, /[aeiou]/, '*')
[].except(:foo, :bar, :baz)
[].map_keys(:id)

Hash

{ a: 1, b: { c: 2 }}.dig(:b, :c)  # => 2
                                  # ii Standard in Ruby >= 2.3
{ a: 1, b: nil }.compact          # => {a: 1}
                                  # !! destructive

Numeric

25.245.to_currency # => "25.25"
10_000.to_duration # => "2 h 46 min"
10_000.to_time_ago # => "2 hours ago"
10.15.to_rad       # => "0.17715091907742445"

String

'hour'.pluralize(2)           # => "hours"
'DJ::JSONStorage'.underscore  # => "dj/json_storage"
'true'.to_boolean             # => "true"
'MiniTest::Test'.constantize  # => MiniTest::Test

Module

Kernel.local_methods  # => methods defined directly in the class + nested constants w/ methods

Object

Object.new.clone!  # => a deep clone of the object (using Marshal.dump)
Object.new.punch   # => a copy of Object.new with String punches mixed in
Object.new.punch!  # => destructive version applies extensions directly to the base object
Object.new.echo    # => prints and returns itself. Accepts a number,
                   #    indicating how many lines of the trace to display
Object.new.track!  # => Trace methods calls to the object
                   # !! requires [object_tracker](https://github.com/ridiculous/object_tracker), which it'll try to download

Method

require 'benchmark'

Benchmark.method(:measure).to_instruct # => the Ruby VM instruction sequence for the method
Benchmark.method(:measure).to_source   # => the method definition as a string

Registering custom punches

DuckPuncher allows you to utilize the punch and punch! interface to decorate or extend, respectively, any 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.

DuckPuncher.register <class>, [<module>, ...]

A full example:

# 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 the extension and punch the class, use DuckPuncher.register!

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)
# =>  ...

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