module HaveAPI::Hooks

All registered hooks and connected endpoints are stored in this module.

It supports connecting to both class and instance level hooks. Instance level hooks inherit all class registered hooks, but it is possible to connect to a specific instance and not for all instances of a class.

Hook definition contains additional information for as a documentation: description, context, arguments, return value.

Every hook can have multiple listeners. They are invoked in the order of registration. Instance-level listeners first, then class-level. Hooks are chained using the block’s first argument and return value. The first block to be executed gets the initial value, may make changes and returns it. The next block gets the return value of the previous block as its first argument, may make changes and returns it. Return value of the last block is returned to the caller of the hook.

Usage

Register hooks

class MyClass
  include Hookable

  has_hook :myhook,
           desc: 'Called when I want to',
           context: 'current',
           args: {
               a: 'integer',
               b: 'integer',
               c: 'integer',
           }
end

Not that the additional information is just optional. A list of defined hooks and their description is a part of the reference documentation generated by yard.

Class level hooks

# Connect hook
MyClass.connect_hook(:myhook) do |ret, a, b, c|
  # a = 1, b = 2, c = 3
  puts "Class hook!"
  ret
end

# Call hooks
MyClass.call_hooks(:myhook, args: [1, 2, 3])

Instance level hooks

# Create an instance of MyClass
my = MyClass.new

# Connect hook
my.connect_hook(:myhook) do |ret, a, b, c|
  # a = 1, b = 2, c = 3
  puts "Instance hook!"
  ret
end

# Call instance hooks
my.call_instance_hooks_for(:myhook, args: [1, 2, 3])
# Call class hooks
my.call_class_hooks_for(:myhook, args: [1, 2, 3])
# Call both instance and class hooks at once
my.call_hooks_for(:myhook, args: [1, 2, 3])

Chaining

5.times do |i|
  MyClass.connect_hook(:myhook) do |ret, a, b, c|
    ret[:counter] += i
    ret
  end
end

p MyClass.call_hooks(:myhook, args: [1, 2, 3], initial: {counter: 0})
=> {:counter=>5}

Constants

INSTANCE_VARIABLE

Public Class Methods

call_for( klass, name, where = nil, args: [], kwargs: {}, initial: {}, instance: nil ) click to toggle source

Call all blocks that are connected to hook in ‘klass` with `name`. klass may be a class name or an object instance. If `where` is set, the blocks are executed in it with instance_exec. `args` is an array of arguments given to all blocks. The first argument to all block is always a return value from previous block or `initial`, which defaults to an empty hash.

Blocks are executed one by one in the order they were connected. Blocks must return a hash, that is then passed to the next block and the return value from the last block is returned to the caller.

A block may decide that no further blocks should be executed. In such a case it calls Hooks.stop with the return value. It is then returned to the caller immediately.

@param klass [Class instance, instance] @param name [Symbol] hook name @param where [Class instance] class in whose context hooks are executed @param args [Array] an array of arguments passed to hooks @param kwargs [Hash] an array of arguments passed to hooks @param initial [Hash] initial return value @param instance [Boolean] call instance hooks or not; nil means auto-detect

# File lib/haveapi/hooks.rb, line 145
def self.call_for(
  klass,
  name,
  where = nil,
  args: [],
  kwargs: {},
  initial: {},
  instance: nil
)
  classified = hook_classify(klass)

  all_hooks = if (instance.nil? && !classified.is_a?(Class)) || instance
                klass.instance_variable_get(INSTANCE_VARIABLE)

              else
                @hooks[classified]
              end

  catch(:stop) do
    return initial unless all_hooks
    return initial unless all_hooks[name]

    hooks = all_hooks[name][:listeners]
    return initial unless hooks

    hooks.each do |hook|
      ret = if where
              where.instance_exec(initial, *args, **kwargs, &hook)

            else
              hook.call(initial, *args, **kwargs)
            end

      initial.update(ret) if ret
    end

    initial
  end
end
connect_hook(klass, name, &block) click to toggle source

Connect class hook defined in ‘klass` with `name` to `block`. `klass` is a class name.

# File lib/haveapi/hooks.rb, line 106
def self.connect_hook(klass, name, &block)
  @hooks[hook_classify(klass)][name][:listeners] << block
end
connect_instance_hook(instance, name, &block) click to toggle source

Connect instance hook from instance ‘klass` with `name` to `block`.

# File lib/haveapi/hooks.rb, line 111
def self.connect_instance_hook(instance, name, &block)
  hooks = instance.instance_variable_get(INSTANCE_VARIABLE)

  unless hooks
    hooks = {}
    instance.instance_variable_set(INSTANCE_VARIABLE, hooks)
  end

  hooks[name] ||= { listeners: [] }
  hooks[name][:listeners] << block
end
hook_classify(klass) click to toggle source
# File lib/haveapi/hooks.rb, line 185
def self.hook_classify(klass)
  klass.is_a?(String) ? Object.const_get(klass) : klass
end
hooks() click to toggle source
# File lib/haveapi/hooks.rb, line 100
def self.hooks
  @hooks
end
register_hook(klass, name, opts = {}) click to toggle source

Register a hook defined by ‘klass` with `name`. @param klass [Class] an instance of Class, that is class name, not it’s instance @param opts [Hash] @option opts [String] :desc why this hook exists, when it’s called @option opts [String] :context the context in which given blocks are called @option opts [Hash] :args hash of block positional arguments @option opts [Hash] :kwargs hash of block keyword arguments @option opts [Hash] :initial - hash of initial values @option opts [Hash] :ret hash of return values

# File lib/haveapi/hooks.rb, line 91
def self.register_hook(klass, name, opts = {})
  classified = hook_classify(klass)
  opts[:listeners] = []

  @hooks ||= {}
  @hooks[classified] ||= {}
  @hooks[classified][name] = opts
end
stop(ret) click to toggle source
# File lib/haveapi/hooks.rb, line 189
def self.stop(ret)
  throw(:stop, ret)
end