Hooks

A thread-safe event handling system for Crystal that supports tagged event routing, ordered callbacks, and flexible error handling.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      hooks:
        github: rubyattack3r/hooks
  2. Run shards install

Usage

Basic Event Handling

require "hooks"

# Create a hook for handling string events
hook = Hooks::Hook(String).new

# Add regular handler
hook.add(Hooks::Handler(String).new { |msg|
  puts "Received: #{msg}"
})

# Add high-priority handler
hook.pre_add(Hooks::Handler(String).new { |msg|
  puts "Pre-processing: #{msg}"
})

# Trigger events
hook.trigger("Hello World!")

# Clean up
hook.remove_all

Tagged Event Handling

require "hooks"

# Define an event class with tags
class UserEvent
  include Hooks::Tagger

  getter tags : Array(String)
  getter data : String

  def initialize(@data : String, @tags : Array(String))
  end
end

# Set up the hooks
base_hook = Hooks::Hook(UserEvent).new
admin_hook = Hooks::TaggedHook(UserEvent).new(base_hook, ["admin"])
user_hook = Hooks::TaggedHook(UserEvent).new(base_hook, ["user"])

# Add handlers for different event types
admin_hook.add(Hooks::Handler(UserEvent).new { |event|
  puts "Admin event: #{event.data}"
})

user_hook.add(Hooks::Handler(UserEvent).new { |event|
  puts "User event: #{event.data}"
})

# Demonstrate event routing
admin_event = UserEvent.new("New admin action", ["admin"])
user_event = UserEvent.new("User logged in", ["user"])
mixed_event = UserEvent.new("Mixed event", ["admin", "user"])

# Events only trigger matching handlers
puts "Triggering admin event:"
admin_hook.trigger(admin_event)

puts "\nTriggering user event:"
user_hook.trigger(user_event)

puts "\nTriggering mixed event:"
admin_hook.trigger(mixed_event)
user_hook.trigger(mixed_event)

Error Handling

require "hooks"

class CustomError < Exception; end

# Set up a hook with some handlers that might fail
hook = Hooks::Hook(String).new

# Add handlers with different error behaviors
hook.add(Hooks::Handler(String).new { |msg|
  puts "Handler 1: #{msg}"
  raise CustomError.new("Something went wrong")
})

hook.add(Hooks::Handler(String).new { |msg|
  puts "Handler 2: #{msg}"
})

hook.add(Hooks::Handler(String).new { |msg|
  puts "Handler 3: #{msg}"
  raise Hooks::StopPropagation.new
})

hook.add(Hooks::Handler(String).new { |_msg|
  puts "Handler 4: Never reached due to StopPropagation"
})

puts "Testing regular error handling (stops on first error):"
result = hook.trigger("test")
if result.is_a?(Exception)
  puts "Error occurred: #{result.message}"
end

puts "\nTesting continuous error handling (collects all errors):"
result = hook.trigger_with_continuation("test")
if result.is_a?(Array(Exception))
  result.each { |error| puts "Error: #{error.message}" }
end

Common Use Cases

Middleware Implementation

Processes data through a series of transformations where each step modifies the input and passes it to the next step. Common in data processing, ETL workflows, and text processing.

require "hooks"

class Pipeline(T)
  getter hook : Hooks::Hook(T)

  def initialize
    @hook = Hooks::Hook(T).new
  end

  def use(&middleware : T -> T)
    @hook.add(Hooks::Handler(T).new do |data|
      middleware.call(data)
    end)
  end

  def process(input : T)
    @hook.trigger(input)
  end
end

# Usage
pipeline = Pipeline(String).new

pipeline.use(&.upcase)
pipeline.use(&.reverse)
pipeline.use { |str| "#{str}!" }

pipeline.process("hello") # => "OLLEH!"

Event Bus Implementation

Implements publish/subscribe messaging using tags for event filtering and generics for type safety. Events are processed asynchronously with error collection.

require "hooks"

# Define our event types
class BaseEvent
  include Hooks::Tagger
  getter tags : Array(String)

  def initialize(@tags : Array(String) = [] of String)
  end
end

class UserCreatedEvent < BaseEvent
  getter username : String

  def initialize(@username : String)
    super(["user", "creation"])
  end
end

class PaymentProcessedEvent < BaseEvent
  getter amount : Float64

  def initialize(@amount : Float64)
    super(["payment", "processed"])
  end
end

# Create our event bus
class EventBus
  getter hook : Hooks::Hook(BaseEvent)

  def initialize
    @hook = Hooks::Hook(BaseEvent).new
  end

  def subscribe(tags : Array(String), &block : BaseEvent -> Nil)
    tagged_hook = Hooks::TaggedHook(BaseEvent).new(@hook, tags)
    tagged_hook.add(Hooks::Handler(BaseEvent).new(block))
  end

  def publish(event : BaseEvent)
    @hook.trigger_with_continuation(event)
  end
end

# Usage
bus = EventBus.new

# Subscribe to different event types
bus.subscribe(["user"]) do |event|
  if event.is_a?(UserCreatedEvent)
    puts "User created: #{event.username}"
  end
end

bus.subscribe(["payment"]) do |event|
  if event.is_a?(PaymentProcessedEvent)
    puts "Payment processed: $#{event.amount}"
  end
end

# Publish events
bus.publish(UserCreatedEvent.new("alice"))
bus.publish(PaymentProcessedEvent.new(99.99))

Channel Implementation

A specialized version of the Event Bus that focuses on named channels, useful for feature-specific event streams.

require "hooks"

class HookChannel(T)
  getter hook : Hooks::Hook(T)
  getter name : String

  def initialize(@name : String)
    @hook = Hooks::Hook(T).new
  end

  def subscribe(&block : T -> Nil)
    @hook.add(Hooks::Handler(T).new(block))
  end

  def publish(message : T)
    @hook.trigger(message)
  end
end

# Usage
chat_channel = HookChannel(String).new("chat")
log_channel = HookChannel(String).new("logs")

chat_channel.subscribe { |msg| puts "[CHAT] #{msg}" }
log_channel.subscribe { |msg| puts "[LOG] #{msg}" }

chat_channel.publish("Hello!") # => [CHAT] Hello!
log_channel.publish("Started") # => [LOG] Started

Design Patterns

Observer Pattern

Implements event subscriptions using thread-safe hooks with automatic error propagation. Handlers can be dynamically added and removed during runtime.

require "hooks"

# Subject/Observable
class Newsletter
  getter hook : Hooks::Hook(String)

  def initialize
    @hook = Hooks::Hook(String).new
  end

  def subscribe(&block : String -> Nil)
    @hook.add(Hooks::Handler(String).new(block))
  end

  def publish(content : String)
    @hook.trigger(content)
  end
end

# Usage
newsletter = Newsletter.new

# Add observers
newsletter.subscribe { |content| puts "Reader 1 received: #{content}" }
newsletter.subscribe { |content| puts "Reader 2 received: #{content}" }

# Publish content
newsletter.publish("New article!")

Chain of Responsibility Pattern

Provides sequential request processing with early termination support through StopPropagation. The trigger_with_continuation method allows collecting all errors across the chain.

require "hooks"

class Request
  include Hooks::Tagger
  getter tags : Array(String)
  getter data : String
  property? processed : Bool

  def initialize(@data : String)
    @tags = [] of String
    @processed = false
  end
end

class RequestPipeline
  getter hook : Hooks::Hook(Request)

  def initialize
    @hook = Hooks::Hook(Request).new
  end

  def add_handler(&block : Request -> Nil)
    @hook.add(Hooks::Handler(Request).new do |request|
      if !request.processed
        block.call(request)
      end
    end)
  end

  def process(request : Request)
    @hook.trigger(request)
  end
end

# Usage
pipeline = RequestPipeline.new

# Add handlers in chain
pipeline.add_handler do |request|
  if request.data.includes?("admin")
    request.processed = true
    puts "Admin handler processed request"
  end
end

pipeline.add_handler do |request|
  if request.data.includes?("user")
    request.processed = true
    puts "User handler processed request"
  end
end

pipeline.add_handler do |request|
  if !request.processed?
    puts "Default handler processed request"
  end
end

# Process requests
pipeline.process(Request.new("admin request"))
pipeline.process(Request.new("user request"))
pipeline.process(Request.new("unknown request"))

Mediator Pattern

Routes messages between components using tag-based filtering. Components receive only events matching their tags, maintaining strict type safety through the generic system.

require "hooks"

class ComponentEvent
  include Hooks::Tagger
  getter tags : Array(String)
  getter source : String
  getter message : String

  def initialize(@source : String, @message : String)
    @tags = [@source]
  end
end

class UIMediator
  getter hook : Hooks::Hook(ComponentEvent)

  def initialize
    @hook = Hooks::Hook(ComponentEvent).new
  end

  def register(component_name : String, &handler : ComponentEvent -> Nil)
    tagged_hook = Hooks::TaggedHook(ComponentEvent).new(@hook, [component_name])
    tagged_hook.add(Hooks::Handler(ComponentEvent).new(handler))
  end

  def notify(source : String, message : String)
    @hook.trigger(ComponentEvent.new(source, message))
  end
end

# Usage
mediator = UIMediator.new

# Register components
mediator.register("button") do |event|
  puts "Button received: #{event.message}"
end

mediator.register("textbox") do |event|
  puts "Textbox received: #{event.message}"
end

# Components communicate through mediator
mediator.notify("button", "clicked")
mediator.notify("textbox", "text changed")

Contributing

  1. Fork it (https://github.com/rubyattack3r/hooks/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors