Class: LittleGhost::Support::Callbacks

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/support/callbacks.rb

Overview

Callbacks lets extensions prepare, replace, or cancel framework work in a predictable order. A later callback sees any replacement made earlier in the chain.

A callback may return Callbacks.continue, Callbacks.cancel, or Callbacks.replace. Any other return value means continue. Replacements become the payload for later callbacks.

Every decision responds to continue?, cancel?, and replace?. A cancellation also exposes reason; a replacement exposes value. Extensions should depend on these methods rather than a decision's concrete class.

callbacks = LittleGhost::Support::Callbacks.new(:prepare)
callbacks.on(:prepare) { |payload| Callbacks.replace(payload.merge(debug: true)) }
decision = callbacks.run(:prepare, {})
decision.value # => {debug: true}

Defined Under Namespace

Classes: Cancel, Continue, Replace

Constant Summary collapse

CONTINUE =

:nodoc:

Continue.new.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*names) ⇒ Callbacks

Starts an empty chain for the declared callback names.



56
57
58
59
# File 'lib/little_ghost/support/callbacks.rb', line 56

def initialize(*names)
  @callbacks = names.to_h { |name| [name.to_sym, []] }
  @prepend_counts = names.to_h { |name| [name.to_sym, 0] }
end

Class Method Details

.cancel(reason = nil) ⇒ Object

Creates a decision whose cancel? predicate indicates that callback processing should stop. The returned value exposes the optional reason.



48
# File 'lib/little_ghost/support/callbacks.rb', line 48

def cancel(reason = nil) = Cancel.new(reason:)

.continueObject

Uses the shared decision whose continue? predicate indicates that callback processing should proceed.



43
# File 'lib/little_ghost/support/callbacks.rb', line 43

def continue = CONTINUE

.replace(value) ⇒ Object

Creates a decision whose replace? predicate indicates that later callbacks should receive value.



52
# File 'lib/little_ghost/support/callbacks.rb', line 52

def replace(value) = Replace.new(value:)

Instance Method Details

#initialize_copy(source) ⇒ Object

Duplicates callback arrays so subclasses and instances can extend a copy.



62
63
64
65
66
# File 'lib/little_ghost/support/callbacks.rb', line 62

def initialize_copy(source)
  super
  @callbacks = source.instance_variable_get(:@callbacks).transform_values(&:dup)
  @prepend_counts = source.instance_variable_get(:@prepend_counts).dup
end

#merge(other) ⇒ Object

Combines this chain with other while preserving prepend ordering.



89
90
91
92
93
94
95
96
97
# File 'lib/little_ghost/support/callbacks.rb', line 89

def merge(other)
  merged = dup
  other.instance_variable_get(:@callbacks).each do |name, callbacks|
    prepend_count = other.instance_variable_get(:@prepend_counts).fetch(name)
    callbacks.first(prepend_count).reverse_each { |callback| merged.on(name, callback, prepend: true) }
    callbacks.drop(prepend_count).each { |callback| merged.on(name, callback) }
  end
  merged
end

#on(name, callable = nil, prepend: false, &block) ⇒ Object

Registers a callable, block, or receiver method name for name.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/little_ghost/support/callbacks.rb', line 69

def on(name, callable = nil, prepend: false, &block)
  callback = callable || block
  unless callback.respond_to?(:call) || callback.is_a?(String) || callback.is_a?(Symbol)
    raise ArgumentError, "A callback is required"
  end

  registered = @callbacks.fetch(name.to_sym) { raise ArgumentError, "Unknown callback: #{name}" }
  unless registered.include?(callback)
    if prepend
      registered.unshift(callback)
      @prepend_counts[name.to_sym] += 1
    else
      registered << callback
    end
  end
  self
end

#run(name, payload, context: nil, receiver: nil) ⇒ Object

Runs name until callbacks finish or one cancels the chain.

The returned decision responds to continue?, cancel?, and replace?. Cancellation decisions expose reason, while replacement decisions expose the final value.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/little_ghost/support/callbacks.rb', line 104

def run(name, payload, context: nil, receiver: nil)
  current = payload
  @callbacks.fetch(name.to_sym) { raise ArgumentError, "Unknown callback: #{name}" }.each do |callback|
    decision = normalize(invoke(callback, current, context, receiver))
    case decision
    when Continue
      next
    when Replace
      current = decision.value
    else
      return decision
    end
  end

  current.equal?(payload) ? self.class.continue : self.class.replace(current)
end