# DuckPuncher [![Gem Version](https://badge.fury.io/rb/duck_puncher.svg)](http://badge.fury.io/rb/duck_puncher) [![Build Status](https://travis-ci.org/ridiculous/duck_puncher.svg)](https://travis-ci.org/ridiculous/duck_puncher) [![Code Climate](https://codeclimate.com/github/ridiculous/duck_puncher/badges/gpa.svg)](https://codeclimate.com/github/ridiculous/duck_puncher) 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 subclass * as a decorator Default extensions: ```ruby Array #m => `[].m(:to_s)` => `[].map(&:to_s)` #mm => `[].mm(:sub, /[aeiou]/, '*')` => `[].map { |x| x.sub(/[aeiou]/, '*') }` #except => `[].except('foo', 'bar')` => `[] - ['foo', 'bar']` Hash #dig => `{a: 1, b: {c: 2}}.dig(:b, :c)` => 2 (Part of standard lib in Ruby >= 2.3) 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 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 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: ```ruby DuckPuncher.punch_all! ``` Punch individual ducks by name: ```ruby DuckPuncher.punch! :Hash, :Object ``` One method to rule them all: ```ruby DuckPuncher.punch! :Object, only: :punch ``` ### Tactical punches Punch only certain methods onto a duck: ```ruby DuckPuncher.punch! :Numeric, only: [:to_currency, :to_duration] ``` The `.punch` method creates and caches a new punched class that __inherits__ from the original: ```ruby >> DuckPuncher.punch :String => DuckPuncher::StringDuck >> DuckPuncher::StringDuck.new('Yes').to_boolean => true >> String.new('Yes').respond_to? :to_boolean => false ``` If you punch `Object` then you can use `punch` on any object to get a new __decorated__ copy of the object with the desired functionality mixed in: ```ruby >> DuckPuncher.punch! :Object, only: :punch >> %w[yes no 1].punch.m(:punch).punch.m(:to_boolean) => [true, false, true] ``` Because `DuckPuncher` extends the amazing [Usable](https://github.com/ridiculous/usable) gem, you can configure only the punches you want! ### Registering custom punches DuckPuncher allows you to utilize the `punch` interface to __decorate__ any kind of object with your own punches. Simply call `.register` with the name of your module (or an array of names) and any of [these options](https://github.com/ridiculous/duck_puncher/blob/master/lib/duck_puncher/duck.rb#L11). ```ruby # 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 ``` ```ruby # Register the extensions DuckPuncher.register [:Billable, :Retryable] # Our duck class User < Struct.new(:name) end # Add the #punch method to User instances DuckPuncher.punch! :Object, only: :punch, target: User # Usage user = User.new('Ryan').punch(:Billable).punch(:Retryable) user.call_with_retry(19.99) ``` Ducks can be registered under any name you want, as long as the `:mod` option specifies a module: ```ruby DuckPuncher.register :bills, mod: 'Admin::Billable' User.new.punch(:bills) ``` When punching at a class level, the `:class` option is required: ```ruby DuckPuncher.register :Billable, class: 'User' DuckPuncher.punch! :Billable ``` ## Install gem 'duck_puncher' ## 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: ```bash >> `require 'pry'` LoadError: cannot load such file -- pry from (irb):1:in `require' from (irb):1 from bin/console:10:in `
' >> DuckPuncher.punch! :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](https://github.com/ridiculous/object_tracker) gem, if it's not available in the current load path, and starts tracking the current object! ```ruby Duck = Class.new Donald = Module.new { def tap_tap() self end } DuckPuncher.punch!(: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, # [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 # [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](https://github.com/ridiculous/duck_puncher) ## License MIT