Reader
Reader is the simplest effect. It passes a value down to the stack.
require 'dry/effects'
class SetLocaleMiddleware
include Dry::Effects::Handler.Reader(:locale)
def initialize(app)
@app = app
end
def call(env)
with_locale(detect_locale(env)) do
@app.(env)
end
end
def detect_locale(env)
# arbitrary detection logic
end
end
### Anywhere in the app
class GreetUser
include Dry::Effects.Reader(:locale)
def call(user)
case locale
when :en then "Hello #{user.name}"
when :de then "Hallo #{user.name}"
when :ru then "Привет, #{user.name}"
when :it then "Ciao #{user.name}"
end
end
end
Testing with Reader
If you run GreetUser#call
without a Reader handler, it will raise an error. For unit tests you'll need some wrapping code:
RSpec.describe GreetUser do
include Dry::Effects::Handler.Reader(:locale)
subject(:greet) { described_class.new }
let(:user) { double(:user, name: 'John') }
it 'uses the current locale to greet the user' do
examples = {
en: 'Hello John',
de: 'Hallo John',
ru: 'Привет, John',
it: 'Ciao John'
}
examples.each do |locale, expected_greeting|
with_locale(locale) do
expect(greet.(user)).to eql(expected_greeting)
end
end
end
end
You can provide locale in an around(:each)
hook:
require 'dry/effects'
# Build a provider object with .call interface
locale_provider = Object.new.extend(Dry::Effects::Handler.Reader(:locale, as: :call))
RSpec.configure do |config|
config.around(:each) do |ex|
locale_provider.(:en, &ex)
end
end
Nesting readers
As a general rule, if there are two handlers in the stack, the nested takes precedence:
require 'dry/effects'
extend Dry::Effects::Handler.Reader(:locale)
extend Dry::Effects.Reader(:locale)
with_locale(:en) { with_locale(:de) { locale } } # => :de
Mixing readers
Every Reader has an identifier. Handlers with different identifiers won't interfere:
require 'dry/effects'
extend Dry::Effects::Handler.Reader(:locale)
extend Dry::Effects::Handler.Reader(:context)
extend Dry::Effects.Reader(:locale)
extend Dry::Effects.Reader(:context)
with_locale(:en) { with_context(:background) { [locale, context] } } # => [:en, :background]
# Order doesn't matter:
with_context(:background) { with_locale(:en) { [locale, context] } } # => [:en, :background]
Relation to State
Reader is part of the State effect.
Tradeoffs of implicit passing
Passing values implicitly is not good or bad by itself; you should consider how it affects your code in every case. Providing the current locale is a good example where reader effect can be justified. On the other hand, passing optional values such as the IP-address of the current user should be done explicitly because they are not always present (consider background jobs, rake tasks, etc.).