summaryrefslogtreecommitdiff
path: root/lib/pry/hooks.rb
blob: 6f5b3fe02b1e8df03b272921c5bc31e4fad126fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
class Pry
  # Implements a hooks system for Pry. A hook is a callable that is associated
  # with an event. A number of events are currently provided by Pry, these
  # include: `:when_started`, `:before_session`, `:after_session`.  A hook must
  # have a name, and is connected with an event by the `Pry::Hooks#add_hook`
  # method.
  #
  # @example Adding a hook for the `:before_session` event.
  #   Pry.config.hooks.add_hook(:before_session, :say_hi) do
  #     puts "hello"
  #   end
  class Hooks
    def initialize
      @hooks = Hash.new { |h, k| h[k] = [] }
    end

    # Ensure that duplicates have their @hooks object.
    def initialize_copy(_orig)
      hooks_dup = @hooks.dup
      @hooks.each do |k, v|
        hooks_dup[k] = v.dup
      end

      @hooks = hooks_dup
    end

    def errors
      @errors ||= []
    end

    # Destructively merge the contents of two `Pry:Hooks` instances.
    #
    # @param [Pry::Hooks] other The `Pry::Hooks` instance to merge
    # @return [Pry:Hooks] The receiver.
    # @see #merge
    def merge!(other)
      @hooks.merge!(other.dup.hooks) do |_key, array, other_array|
        temp_hash, output = {}, []

        (array + other_array).reverse_each do |pair|
          temp_hash[pair.first] ||= output.unshift(pair)
        end

        output
      end

      self
    end

    # @example
    #   hooks = Pry::Hooks.new.add_hook(:before_session, :say_hi) { puts "hi!" }
    #   Pry::Hooks.new.merge(hooks)
    # @param [Pry::Hooks] other The `Pry::Hooks` instance to merge
    # @return [Pry::Hooks] a new `Pry::Hooks` instance containing a merge of the
    #   contents of two `Pry:Hooks` instances.
    def merge(other)
      self.dup.tap do |v|
        v.merge!(other)
      end
    end

    # Add a new hook to be executed for the `event_name` event.
    # @param [Symbol] event_name The name of the event.
    # @param [Symbol] hook_name The name of the hook.
    # @param [#call] callable The callable.
    # @yield The block to use as the callable (if no `callable` provided).
    # @return [Pry:Hooks] The receiver.
    def add_hook(event_name, hook_name, callable = nil, &block)
      event_name = event_name.to_s

      # do not allow duplicates, but allow multiple `nil` hooks
      # (anonymous hooks)
      if hook_exists?(event_name, hook_name) && !hook_name.nil?
        raise ArgumentError, "Hook with name '#{hook_name}' already defined!"
      end

      if !block && !callable
        raise ArgumentError, "Must provide a block or callable."
      end

      # ensure we only have one anonymous hook
      @hooks[event_name].delete_if { |h, _k| h.nil? } if hook_name.nil?

      if block
        @hooks[event_name] << [hook_name, block]
      elsif callable
        @hooks[event_name] << [hook_name, callable]
      end

      self
    end

    # Execute the list of hooks for the `event_name` event.
    # @param [Symbol] event_name The name of the event.
    # @param [Array] args The arguments to pass to each hook function.
    # @return [Object] The return value of the last executed hook.
    def exec_hook(event_name, *args, &block)
      @hooks[event_name.to_s].map do |_hook_name, callable|
        begin
          callable.call(*args, &block)
        rescue RescuableException => e
          errors << e
          e
        end
      end.last
    end

    # @param [Symbol] event_name The name of the event.
    # @return [Fixnum] The number of hook functions for `event_name`.
    def hook_count(event_name)
      @hooks[event_name.to_s].size
    end

    # @param [Symbol] event_name The name of the event.
    # @param [Symbol] hook_name The name of the hook
    # @return [#call] a specific hook for a given event.
    def get_hook(event_name, hook_name)
      hook = @hooks[event_name.to_s].find do |current_hook_name, _callable|
        current_hook_name == hook_name
      end
      hook.last if hook
    end

    # @param [Symbol] event_name The name of the event.
    # @return [Hash] The hash of hook names / hook functions.
    # @note Modifying the returned hash does not alter the hooks, use
    # `add_hook`/`delete_hook` for that.
    def get_hooks(event_name)
      Hash[@hooks[event_name.to_s]]
    end

    # @param [Symbol] event_name The name of the event.
    # @param [Symbol] hook_name The name of the hook.
    #   to delete.
    # @return [#call] The deleted hook.
    def delete_hook(event_name, hook_name)
      deleted_callable = nil

      @hooks[event_name.to_s].delete_if do |current_hook_name, callable|
        if current_hook_name == hook_name
          deleted_callable = callable
          true
        else
          false
        end
      end
      deleted_callable
    end

    # Clear all hooks functions for a given event.
    #
    # @param [String] event_name The name of the event.
    # @return [void]
    def clear_event_hooks(event_name)
      @hooks[event_name.to_s] = []
    end

    # @param [Symbol] event_name Name of the event.
    # @param [Symbol] hook_name Name of the hook.
    # @return [Boolean] Whether the hook by the name `hook_name`.
    def hook_exists?(event_name, hook_name)
      @hooks[event_name.to_s].map(&:first).include?(hook_name)
    end

    protected

    def hooks
      @hooks
    end
  end
end