Medi8 is a lightweight, idiomatic mediator pattern implementation for Ruby and Rails, inspired by MediatR (from .NET)
Medi8 is not a 1:1 Ruby analog of MediatR, but it faithfully implements its core principles in an idiomatic Ruby way.
Medi8 is compatible with pure Ruby, Sinatra, and Rails.
- Simple
send
method for commands and queries publish
notifications to many subscribers- Middleware pipeline
- Async notifications with ActiveJob
- Rails integration via
Railtie
- Fully modular, no inheritance required
@startuml
title Medi8 Logical Flow and Event Sequence
actor "Caller (e.g. Controller)" as Caller
participant "Medi8" as Medi8
participant "Mediator" as Mediator
participant "RegisterUserHandler" as Handler
participant "Medi8.publish" as Publisher
participant "SendWelcomeEmailHandler" as Emailer
participant "TrackRegistrationHandler" as Tracker
== Command Dispatch ==
Caller -> Medi8: send(RegisterUser)
Medi8 -> Mediator: new(registry)\nsend(request)
Mediator -> Handler: call(request)
== Event Publishing ==
Handler -> Medi8: publish(UserRegistered)
Medi8 -> Mediator: new(registry)\npublish(event)
Mediator -> Publisher: NotificationDispatcher.publish(event)
== Notify Subscribers ==
Publisher -> Emailer: call(event)
Publisher -> Tracker: call(event)
@enduml
Add this line to your Gemfile:
gem "medi8-rb"
Then bundle:
$ bundle install
Or install it directly:
$ gem install medi8-rb
Use Medi8.send(request)
for command/query behavior, and Medi8.publish(event)
for notifications. Handlers are registered with a simple DSL.
Set up in an initializer:
# config/initializers/medi8.rb
Medi8.configure do |config|
config.use AwesomeMiddleware
end
Example middleware:
class AwesomeMiddleware
def call(request)
Rails.logger.info("Processing #{request.class.name}")
yield
end
end
Medi8 uses a simple request/handler model. You define a request class, register a handler using handles
, and then invoke Medi8.send(request)
.
# app/requests/create_user.rb
class CreateUser
attr_reader :name
def initialize(name:)
@name = name
end
end
# app/handlers/create_user_handler.rb
class CreateUserHandler
include Medi8::Handler
handles CreateUser
def call(request)
User.create!(name: request.name)
end
end
Medi8.send(CreateUser.new(name: "Alice"))
class UserRegistered
attr_reader :user_id
def initialize(user_id:)
@user_id = user_id
end
end
class SendWelcomeEmail
include Medi8::NotificationHandler
subscribes_to UserRegistered, async: true
def call(event)
UserMailer.welcome_email(User.find(event.user_id)).deliver_later
end
end
Medi8.publish(UserRegistered.new(user_id: 1))
Medi8 is compatible with Active CQRS.
For project layout consistency, use events
and handlers/events
for your Medi8 classes. This will align nicely with the expected folder structures of Active CQRS. If following this advice, remember to auto-load these directories.
config.autoload_paths += %W[
#{config.root}/app/events
#{config.root}/app/handlers/events
]
config.eager_load_paths += %W[
#{config.root}/app/events
#{config.root}/app/handlers/events
]
Alternatively, namespace appropriately if you prefer nested classes.
Example Active CQRS command:
# app/commands/create_user_command.rb
class CreateUserCommand
attr_reader :name, :email
def initialize(name:, email:)
@name = name
@email = email
end
end
Active CQRS handler with Medi8:
# app/handlers/commands/create_user_handler.rb
class CreateUserHandler
def call(command)
user = User.create!(name: command.name, email: command.email)
Medi8.publish(UserRegistered.new(user_id: user.id, email: user.email))
user
end
end
Medi8 notification published from the handler:
# app/events/user_registered.rb
class UserRegistered
attr_reader :user_id, :email
def initialize(user_id:, email:)
@user_id = user_id
@email = email
end
end
Medi8 notification handler(s):
# app/handlers/events/send_welcome_email_handler.rb
class SendWelcomeEmailHandler
include Medi8::NotificationHandler
subscribes_to UserRegistered
def call(event)
UserMailer.welcome_email(event.email).deliver_later
end
end
# app/handlers/events/track_user_registration_handler.rb
class TrackUserRegistrationHandler
include Medi8::NotificationHandler
subscribes_to UserRegistered
def call(event)
Rails.logger.info("=> User registered: #{event.user_id} (#{event.email})")
end
end
Example controller:
class UsersController < ApplicationController
def create
command = CreateUserCommand.new(
name: params[:name],
email: params[:email]
)
user = CQRS_COMMAND_BUS.call(command)
render json: user, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
end
$ bundle exec rspec
Provided is an end-to-end mock flow under mock_e2e
. Test this from the terminal.
ruby mock_e2e.rb
MIT License © kiebor81
Bug reports and pull requests welcome.