Module: HaveAPI::Hooks

Defined in:
lib/haveapi/hooks.rb

Overview

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}

Constant Summary collapse

INSTANCE_VARIABLE =
'@_haveapi_hooks'

Class Method Summary collapse

Class Method Details

.call_for(klass, name, where = nil, args: [], initial: {}, instance: nil) ⇒ Object

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.

Parameters:

  • klass (Class instance, instance)
  • name (Symbol)

    hook name

  • where (Class instance) (defaults to: nil)

    class in whose context hooks are executed

  • args (Array) (defaults to: [])

    an array of arguments passed to hooks

  • initial (Hash) (defaults to: {})

    initial return value

  • instance (Boolean) (defaults to: nil)

    call instance hooks or not; nil means auto-detect



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/haveapi/hooks.rb', line 143

def self.call_for(
    klass,
    name,
    where = nil,
    args: [],
    initial: {},
    instance: nil
)
  classified = hook_classify(klass)

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

  else
    all_hooks = @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|
      if where
        ret = where.instance_exec(initial, *args, &hook)

      else
        ret = hook.call(initial, *args)
      end

      initial.update(ret) if ret
    end

    initial
  end
end

.connect_hook(klass, name, &block) ⇒ Object

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



105
106
107
# File 'lib/haveapi/hooks.rb', line 105

def self.connect_hook(klass, name, &block)
  @hooks[hook_classify(klass)][name][:listeners] << block
end

.connect_instance_hook(instance, name, &block) ⇒ Object

Connect instance hook from instance klass with name to block.



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/haveapi/hooks.rb', line 110

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) ⇒ Object



181
182
183
# File 'lib/haveapi/hooks.rb', line 181

def self.hook_classify(klass)
  klass.is_a?(String) ? Object.const_get(klass) : klass
end

.hooksObject



99
100
101
# File 'lib/haveapi/hooks.rb', line 99

def self.hooks
  @hooks
end

.register_hook(klass, name, opts = {}) ⇒ Object

Register a hook defined by klass with name. klass is an instance of Class, that is class name, not it’s instance. opts is a hash and can have following keys:

- desc - why this hook exists, when it's called
- context - the context in which given blocks are called
- args - hash of block arguments
- initial - hash of initial values
- ret - hash of return values


90
91
92
93
94
95
96
97
# File 'lib/haveapi/hooks.rb', line 90

def self.register_hook(klass, name, opts = {})
  classified = hook_classify(klass)
  opts[:listeners] = []

  @hooks ||= {}
  @hooks[classified] ||= {}
  @hooks[classified][name] = opts
end

.stop(ret) ⇒ Object



185
186
187
# File 'lib/haveapi/hooks.rb', line 185

def self.stop(ret)
  throw(:stop, ret)
end