DurableDecorator
This is a project for modifying the behavior of gems outside of your reach. You may be using a large Rails Engine and be wanting to simply decorate some existing behavior, but at the same time you want to inherit original behavior.
On tracking new decorators and managing fragility
After a lovely and short discussion with Brian Quinn regarding these ideas, he mentioned we could try hashing methods to be able to raise warnings upon unexpected sources or targets (see his work on Deface). This project relies on another lovely meta-programming creation by John Mair, specifically his work on method_source.
Some additional background: http://stackoverflow.com/questions/4470108/when-monkey-patching-a-method-can-you-call-the-overridden-method-from-the-new-i
Installation
Add this line to your application's Gemfile:
gem 'durable_decorator', github: 'jumph4x/durable_decorator'
Or to include rake tasks for Rails you can use DurableDecoratorRails:
gem 'durable_decorator_rails', github: 'jumph4x/durable_decorator_rails'
And then execute:
$ bundle
UPGRADING
Prior to Version 0.2.0 original methods would have a suffix of original or SHA. A recent change has been made to use prefix rather than suffix in order to be compatible with example! and example? methods. Please review your durably decorated methods when upgrading to Version 0.2.0.
Versions >= 0.2.0 are not tested on Rubies < 1.9.2. Please use version 0.1.2 if you absolutely need Ruby 1.8.7 compatibility.
Usage
class ExampleClass
def string_method
"original"
end
end
ExampleClass.class_eval do
durably_decorate :string_method do
original_string_method + " and new"
end
end
instance = ExampleClass.new
instance.string_method
# => "original and new"
Working with SHAs
Furthermore, we can hash the contents of a method as it exists at inspect-time and seal it by providing extra options to the decorator. If the method definition gets tampered with, the decorator will detect this at decoration-time and raise an error for your review.
Find the SHA of the method as currently loaded into memory, works with classes as well as modules:
DurableDecorator::Base.determine_sha('ExampleClass#instance_method')
Or for class (singleton) methods:
DurableDecorator::Base.determine_sha('ExampleClass.class_level_method')
Armed with this knowledge, we can enforce a strict mode:
DurableDecorator::Base.determine_sha('ExampleClass#no_param_method')
# => 'ba3114b2d46caa684b3f7ba38d6f74b2'
ExampleClass.class_eval do
durably_decorate :string_method, mode: 'strict', sha: 'WRONG-SHA-123456' do
original_string_method + " and new"
end
end
DurableDecorator::TamperedDefinitionError: Method SHA mismatch, the definition has been tampered with
DurableDecorator may also decorate methods with params like so:
class ExampleClass
def string_method(text)
"original #{text}"
end
end
ExampleClass.class_eval do
durably_decorate :string_method, mode: 'strict', sha: 'ba3114b2d46caa684b3f7ba38d6f74b2' do |text|
original_string_method(text) + " and new"
end
end
instance = ExampleClass.new
instance.string_method('test')
# => "original test and new"
DurableDecorator also maintains explicit versions of each method overriden by creating aliases with prepended SHAs of the form _1234abcd_some_method
so you can always target explicit method versions without relying on original_some_method
.
DurableDecorator maintains 3 versions of aliases to previous method versions, 2 of which are short-SHA versions, akin to Github:
DurableDecorator::Base.determine_sha('ExampleClass#no_param_method')
# => 'ba3114b2d46caa684b3f7ba38d6f74b2'
ExampleClass.class_eval do
durably_decorate :string_method do
"new"
end
end
# 3 explicit aliases preserve access to the original method based on it's original SHA:
# 4-char SHA, 6-char SHA and the full SHA prefix
instance = ExampleClass.new
instance._ba31_string_method
# => "original"
instance._ba3114_string_method
# => "original"
instance._ba3114b2d46caa684b3f7ba38d6f74b2_string_method
# => "original"
Asking for history
You can inquire about the history of method [re]definitions like this:
DurableDecorator::Base.definitions['ExampleClass#one_param_method']
# => [{:name=>:one_param_method, :sha=>"935888f04d9e132be458591d5755cb8131fec457", :body=>"def one_param_method param\n \"original: \#{param}\"\nend\n", :source=>["/home/denis/rails/durable_decorator/spec/example_class.rb", 6]}, {:name=>:one_param_method, :sha=>"3c39948e5e83c04fd4bf7a6ffab12c6828e0d959", :body=>"durably_decorate :one_param_method do |another_string|\n \"\#{one_param_method_935888f04d9e132be458591d5755cb8131fec457('check')} and \#{another_string}\"\nend\n", :source=>["/home/denis/rails/durable_decorator/spec/durable_decorator_spec.rb", 45]}]
With any luck you can even get the specific [re]definition printed!
puts DurableDecorator::Base.definitions['ExampleClass#one_param_method'][0][:body]
def one_param_method param
"original: #{param}"
end
No more surprise monkey patching
Once you decorate the method and seal it with its SHA, if some gem tries to come in and overwrite your work BEFORE decorate-time, DurableDecorator will warn you. Similarly, expect to see an exception bubble up if the definition of the original method has changed and requires a review and a re-hash.
The usefulness is for gem consumers, and their application-level specs.
Problems
Currently, dealing with default parameter values is problematic due to how Ruby answer inquiries about methods' arity. If the method you are overriding has default values, consider appending a splat argument and manually parsing the contents. Here is an example:
class ExampleClass
def string_method arg1, args2 = [], arg3 = {}
"original"
end
end
ExampleClass.class_eval do
durably_decorate :string_method do |arg1, *args|
arg2 = args[0] || []
arg3 = args[1] || {}
original_string_method + " and new"
end
end
Additionally, if the method uses def-level exception rescuing, you will likely need to wrap it in a begin
-```end``, consider such a class:
class ExampleClass
def string_method
ExceptionRaiser.call
rescue
'Failure'
end
end
In this case, running the generation will produce incomplete syntax:
ExampleClass.class_eval do
durably_decorate :string_method do
ExceptionRaiser.call
rescue
'Failure'
end
end
Correct like so:
ExampleClass.class_eval do
durably_decorate :string_method do
begin
ExceptionRaiser.call
rescue
'Failure'
end
end
end
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Cred
A project by Downshift Labs, Ruby on Rails, Performance tuning and Spree Commerce projects.