Skip to content

okuramasafumi/tiny_hooks

Repository files navigation

Ruby

TinyHooks

A tiny gem to define hooks.

Installation

Add this line to your application's Gemfile:

gem 'tiny_hooks'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install tiny_hooks

Usage

include TinyHooks in your class/module and you're all set to use define_hook!

class MyClass
  include TinyHooks

  def my_method
    puts 'my method'
  end

  define_hook :before, :my_method do
    puts 'my before hook'
  end
end

MyClass.new.my_method
# => "my before hook\nmy method\n"

You can also call define_hook with method name as a third argument.

class MyClass
  include TinyHooks

  def my_method
    puts 'my method'
  end

  def my_before_hook
    puts 'my before hook'
  end

  define_hook :before, :my_method, :my_before_hook
end

MyClass.new.my_method
# => "my before hook\nmy method\n"

You can define hooks for class methods as well.

class MyClass
  include TinyHooks

  def self.my_method
    puts 'my method'
  end

  define_hook :before, :my_method, class_method: true do
    puts 'my before hook'
  end
end

MyClass.my_method
# => "my before hook\nmy method\n"

TinyHooks shines when the class/module is the base class/module of your library and your users will inherit/include it. In these cases, end users can define hooks to the methods you provide. The only thing you have to do is to provide the list of methods.

Halting

You can halt hook and method body execution by throwing :abort.

class MyClass
  include TinyHooks

  def my_method
    puts 'my method'
  end

  define_hook :before, :my_method do
    throw :abort
    puts 'my before hook'
  end
end

MyClass.new.my_method
# => ""

You can change how to halt from two options: throwing :abort and returning false. This can be done via terminator option.

class MyClass
  include TinyHooks

  def my_method
    puts 'my method'
  end

  define_hook :before, :my_method, terminator: :return_false do
    false
  end
end

MyClass.new.my_method
# => ""

Targeting for hooks

You can limit the targets for hooks in two ways. You can enable hooks for public methods only by using public_only! method and include/exclude targets with Regexp pattern by using targets! method.

class MyClass
  include TinyHooks

  def my_method
    puts 'my method'
  end

  private

  def my_private_method
    puts 'my private method'
  end
end

class MyClassWithPublicOnly < MyClass
  public_only!

  define_hook :before, :my_private_method do
    puts 'my_private_method'
  end
  # => This causes PrivateError
end

class MyClassWithExclude < MyClass
  target! exclude_pattern: /my_method/

  define_hook :before, :my_method do
    puts 'my_method'
  end
  # => This causes TargetError
end

You can call include_private! method to disable the effect of public_only!.

Conditional hooks

You can add if option to define_hook call. if option must be a Proc and is evaluated in context of an instance.

class MyClass
  include TinyHooks

  def initialize(hook_enabled = true)
    @hook_enabled = hook_enabled
  end

  def my_method
    puts 'my method'
  end

  def hook_enabled?
    @hook_enabled
  end

  define_hook :before, :my_method, if: -> { hook_enabled? } do
    puts 'my before hook'
  end
end

MyClass.new(true).my_method
# => "my before hook\nmy method\n"

MyClass.new(false).my_method
# => "my method\n"

Differences between TinyHooks and ActiveSupport::Callbacks

While TinyHooks and ActiveSupport::Callbacks share the same purpose, there are a few major differences.

Differences in usage

  • While ActiveSupport::Callbacks has a set of methods for callbacks to work, TinyHooks has only one method.
  • You can apply callbacks/hooks into any existing methods without any changes with TinyHooks, while you need to change methods to call run_callbacks method within them to apply callbacks with ActiveSupport::Callbacks.

Differences in performance

According to the benchmark, TinyHooks is 1.6 times as fast as ActiveSupport::Callbacks when before and after callbacks are applied, and twice as fast when no callbacks are applied.

The result on my machine:

Warming up --------------------------------------
       ActiveSupport   246.181k i/s -    256.956k times in 1.043769s (4.06μs/i)
           TinyHooks   282.834k i/s -    293.502k times in 1.037719s (3.54μs/i)
Calculating -------------------------------------
       ActiveSupport   230.196k i/s -    738.542k times in 3.208320s (4.34μs/i)
           TinyHooks   373.057k i/s -    848.501k times in 2.274453s (2.68μs/i)

Comparison:
           TinyHooks:    373057.2 i/s
       ActiveSupport:    230195.9 i/s - 1.62x  slower

Warming up --------------------------------------
ActiveSupport no callback set     1.992M i/s -      2.096M times in 1.052258s (501.99ns/i)
    TinyHooks no callback set     3.754M i/s -      3.791M times in 1.009753s (266.39ns/i)
                        Plain     3.852M i/s -      3.955M times in 1.026654s (259.57ns/i)
Calculating -------------------------------------
ActiveSupport no callback set     2.005M i/s -      5.976M times in 2.980861s (498.79ns/i)
    TinyHooks no callback set     4.025M i/s -     11.262M times in 2.798054s (248.46ns/i)
                        Plain     3.765M i/s -     11.557M times in 3.069944s (265.63ns/i)

Comparison:
    TinyHooks no callback set:   4024854.4 i/s
                        Plain:   3764695.4 i/s - 1.07x  slower
ActiveSupport no callback set:   2004848.9 i/s - 2.01x  slower

Differences in functionality

There are few things TinyHooks doesn't cover. For example, TinyHooks doesn't support unless option in define_hook method or Symbol as a callback body since they are just syntax sugar.

One of the features TinyHooks doesn't have is reset_callbacks which resets all callbacks with given condition. In order to do this, you must call restore_original method in iteration.

Conclusion

In short, in most cases, TinyHooks is simpler, easier and faster solution.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/okuramasafumi/tiny_hooks. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the TinyHooks project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.