Hooks Ruby Library

repository·master·Indexed 43 days ago

https://github.com/apotonick/hooks

A lightweight Ruby library for defining declarative hooks and callbacks within classes. It provides a simpler alternative to ActiveSupport::Callbacks, supporting method-based or block-based callbacks, custom execution scopes, and the ability to halt execution chains via the :halts_on_falsey option. Includes Hooks::InstanceHooks for per-object callback definitions.

Tokens
3.3K
Snippets
15
Records
17
Agent score
54%

What's inside Hooks

  1. Use Instance Hooks for per-object callbacks

    master

    If you need to add callbacks to a specific instance of a class without affecting the entire class, include Hooks::InstanceHooks. This allows you to call define_hook and callback methods (like after_dark) directly on an object instance.

    class Cat
      include Hooks
      include Hooks::InstanceHooks
    
      define_hook :after_dark
      after_dark { "Chase mice" }
    end
    
    # Instance-specific extension
    garfield = Cat.new
    garfield.after_dark :sleep # Adds :sleep to this instance only
    garfield.run_hook(:after_dark) # => ["Chase mice", :sleep]
    
    # Adding a new hook to an instance
    garfield.define_hook :before_six
    garfield.before_six { puts "Early!" }
  2. Define and use basic hooks

    master

    To use Hooks, include the Hooks module in your class and use define_hooks (plural) or define_hook (singular) to declare the hook names. You can then add callbacks using before_<hook_name> or after_<hook_name> methods, or by passing a block to the hook name method.

    Callbacks can be instance methods or blocks. When a hook is run via run_hook, all registered callbacks are executed in the context of the instance (self refers to the instance).

    require 'hooks'
    
    class Cat
      include Hooks
    
      # Declare hooks
      define_hooks :before_dinner, :after_dinner
    
      # Add callback as a method
      before_dinner :wash_paws
    
      # Add callback as a block
      after_dinner do
        puts "Ice cream for #{self}!"
      end
    
      def wash_paws
        puts "Washing paws..."
      end
    end
    
    cat = Cat.new
    cat.run_hook :after_dinner
  3. How hooks and callbacks work together

    master

    The Hooks module follows a declarative pattern for managing lifecycle events:

    1. Declaration: Use define_hooks :name to initialize a hook.
    2. Registration: Use the dynamically generated method name(method_or_block) to add callbacks. Callbacks can be a Symbol (referring to a method) or a Block.
    3. Execution: Use run_hook :name on an instance to trigger the callbacks in the instance context, or on a class to trigger class-level callbacks.

    This pattern is designed to be a lightweight alternative to ActiveSupport::Callbacks.

  4. Integrate Hooks into your classes

    master

    To use the Hooks functionality, include the Hooks module in your class. This provides the define_hooks method for declaring hooks and various methods for registering callbacks and running them.

    Once included, you can use define_hooks to initialize a set of hooks, then use the automatically generated writer methods (named after the hook) to register blocks or method names as callbacks.

    class MyClass
      include Hooks
    
      define_hooks :after_save
    
      # Register a block
      after_save { puts "Saved!" }
    
      # Register a method
      after_save :log_change
    
      def log_change
        puts "Change logged"
      end
    end
  5. Halt a hook execution chain

    master

    By default, run_hook executes all callbacks. You can change this behavior by using the halts_on_falsey: true option in define_hook. When this option is enabled, the callback chain will stop immediately if any callback returns nil or false.

    run_hook returns an array of the results from the successful callbacks. You can call .halted? on this result array to check if the chain was stopped early.

    class Garfield
      include Hooks
      # Enable halting on nil/false
      define_hook :after_dark, halts_on_falsey: true
    
      after_dark { "Chase mice" }
      after_dark { nil }
      after_dark { "Enjoy supper" }
    end
    
    result = Garfield.new.run_hook :after_dark
    # => ["Chase mice"]
    
    result.halted? # => true
  6. Pass arguments to hook callbacks

    master

    When invoking a hook with run_hook, you can pass additional arguments. These arguments are passed directly to the callback methods or blocks. Ensure your callback signatures are prepared to receive these parameters.

    # Definition
    class Cat
      include Hooks
      define_hook :before_dinner
    
      before_dinner do |who, when|
        puts "#{who} eats at #{when}"
      end
    end
    
    # Invocation
    cat = Cat.new
    cat.run_hook :before_dinner, cat, Time.now
  7. Change callback execution scope

    master

    Normally, callbacks execute in the context of the object instance. You can override this by providing a :scope option to define_hook. The :scope option accepts a lambda that is called for every callback.

    • The lambda receives two arguments: callback and scope.
    • The lambda should return the object context in which the callback should run.
    • Returning nil from the lambda reverts to the original instance context.
    # Execute callbacks in the Logger class context
    define_hook :before_lunch, scope: lambda { |callback, scope| Logger }
    
    # Execute in original context
    define_hook :before_lunch, scope: lambda { |*| nil }
  8. Configure halting behavior with `:halts_on_falsey`

    master

    When defining a hook, you can pass options to control the execution flow. The :halts_on_falsey option determines if the hook chain should stop executing as soon as a callback returns a falsey value.

    • Default behavior: All callbacks are executed regardless of their return value.
    • :halts_on_falsey => true: The chain stops at the first callback that returns false or nil.
    define_hook :my_hook, :halts_on_falsey => true
  9. Run a hook chain with `Hooks::Hook#run`

    master

    The run method executes a sequence of callbacks registered in a hook. It returns a Hooks::Hook::Results object, which acts like an array containing the return values of each executed callback (the chain).

    By default, all callbacks in the chain are executed. However, if the hook is configured with :halts_on_falsey => true, the execution will stop immediately if any callback returns a falsey value.

    class Person
      define_hook :before_eating
    
      before_eating :wash_hands
      before_eating :locate_food
      before_eating :sit_down
    
      def wash_hands; :washed_hands; end
      def locate_food; :located_food; false; end
      def sit_down; :sat_down; end
    end
    
    person = Person.new
    result = person.run_hook(:before_eating)
    # result.chain => [:washed_hands, false, :sat_down]
    
    # If configured with :halts_on_falsey => true:
    # class Person
    #   define_hook :before_eating, :halts_on_falsey => true
    #   ...
    # end
    #
    # result = person.run_hook(:before_eating)
    # result.chain => [:washed_hands]
  10. Run instance hooks with `run_hook`

    master

    To execute the callbacks associated with a specific hook on an instance, call run_hook(name, *args). Any additional arguments passed to run_hook will be passed directly to the registered callbacks.

    Example:

    cat = Cat.new
    cat.run_hook :after_dinner, "i want ice cream!"

    If a callback is a block, it receives the arguments. If it is a method, the method is invoked with those arguments.

    cat.run_hook :after_dinner, "i want ice cream!"
  11. Retrieve callbacks with `callbacks_for_hook`

    master

    If you need to manually manage or execute callbacks in a different context, use callbacks_for_hook(name). This returns the collection of callbacks for the specified hook.

    Callbacks are wrapped in Uber::Option::Value objects, which provide an evaluate(scope, *args) method. This allows you to execute the callback without needing to know if it is a block, a static value, or a method name.

    Example:

    # Runs callbacks in the object instance context with "create" as the argument
    self.class.callbacks_for_hook(:after_eight).each do |callback|
      callback.evaluate(self, "create")
    end
    self.class.callbacks_for_hook(:after_eight).each do |callback|
      callback.evaluate(self, "create")
    end