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'.freeze

Class Method Summary collapse

Class Method Details

.call_for(klass, name, where = nil, args: [], kwargs: {}, 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

  • kwargs (Hash) (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



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
180
181
182
183
# 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) ⇒ Object

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



106
107
108
# 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) ⇒ Object

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



111
112
113
114
115
116
117
118
119
120
121
# 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) ⇒ Object



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

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

.hooksObject



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

def self.hooks
  @hooks
end

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

Register a hook defined by ‘klass` with `name`.

Parameters:

  • klass (Class)

    an instance of Class, that is class name, not it’s instance

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

Options Hash (opts):

  • :desc (String)

    why this hook exists, when it’s called

  • :context (String)

    the context in which given blocks are called

  • :args (Hash)

    hash of block positional arguments

  • :kwargs (Hash)

    hash of block keyword arguments

  • :initial (Hash)
    • hash of initial values

  • :ret (Hash)

    hash of return values



91
92
93
94
95
96
97
98
# 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) ⇒ Object



189
190
191
# File 'lib/haveapi/hooks.rb', line 189

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