Hooks
A thread-safe event handling system for Crystal that supports tagged event routing, ordered callbacks, and flexible error handling.
Installation
-
Add the dependency to your
shard.yml
:dependencies: hooks: github: rubyattack3r/hooks
-
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
- Fork it (https://github.com/rubyattack3r/hooks/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Contributors
- rubyattack3r - creator and maintainer