Resolve (Dependency Injection)
Resolve is an effect for injecting dependencies. A simple usage example:
require 'dry/effects'
class CreateUser
include Dry::Effects.Resolve(:user_repo)
def call(values)
name = values.values_at(:first_name, :last_name).join(' ')
user_repo.create(**values.merge(name: name))
end
end
Providing user_repo
in tests:
RSpec.describe CreateUser do
# adds #provide
include Dry::Effects::Handler.Resolve
subject(:create_user) { described_class.new }
let(:user_repo) { double(:user_repo) }
it 'creates a user' do
expect(user_repo).to receive(:create).with(
first_name: 'John',
last_name: 'Doe',
name: 'John Doe'
)
provide(user_repo: user_repo) { create_user.(first_name: 'John', last_name: 'Doe') }
end
end
Providing dependencies with middleware:
class ProviderMiddleware
include Dry::Effects::Handler.Resolve
def initialize(app, dependencies)
@app = app
@dependencies = dependencies
end
def call(env)
provide(@dependencies) { @app.(env) }
end
end
Then in config.ru
:
# ...some bootstrapping code ...
use ProviderMiddleware, user_repo: UserRepo.new
run Application.new
Compatibility with dry-container
and dry-system
Any object that responds to .key?
and .[]
can be used for providing dependencies. Thus, the default Resolve provider is compatible with dry-container
and dry-system
out of the box.
def call(env)
# Assuming App is a subclass of Dry::System::Container
provide(App) { @app.(env) }
end
Providing static values
One can pass a container to the module builder:
class ProviderMiddleware
include Dry::Effects::Handler.Resolve(Application)
def initialize(app)
@app = app
end
def call(env)
# Here Application will be used for resolving dependencies
provide { @app.(env) }
end
end
Injecting many keys and using aliases
require 'dry/effects'
class CreateUser
include Dry::Effects.Resolve(
# Injected as .schema
# but resolved with 'operations.create_user.schema'
'operations.create_user.schema',
# Injected as .repo
# but resolved with 'repos.user_repo'
repo: 'repos.user_repo'
)
def call(values)
result = schema.(values)
if result.success?
user = repo.create(result.to_h)
[:ok, user]
else
[:err, result]
end
end
end
Overriding dependencies in test environment
Sometimes you may want to push dependencies through an existing handler. This is normally needed for testing when you want to replace some dependencies in a test environment for an assembled app, like a Rack application. Passing overridable: true
enables it:
require 'dry/effects'
class ProviderMiddleware
include Dry::Effects::Handler.Resolve
def initialize(app)
@app = app
end
def call(env)
provide(Application, overridable: overridable?) { @app.(env) }
end
def overridable?
ENV['RACK_ENV'].eql?('test')
end
end
Now in tests, you can override some dependencies at will:
require 'dry/effects'
require 'rack/test'
RSpec.describe do
include Rack::Test::Methods
include Dry::Effects::Handler.Provider
let(:app) do
# building an assembled rack app
end
describe 'POST /users' do
let(:user_repo) { double(:user_repo) }
it 'creates a user' do
expect(user_repo).to receive(:create).with(
first_name: 'John', last_name: 'Doe'
).and_return(1)
# Overriding one dependency
# It will only work if `overridable: true` is passed
# in the middleware
provide('repos.user_repo' => user_repo) do
post(
'/users',
JSON.dump(first_name: 'John', last_name: 'Doe'),
'CONTENT_TYPE' => 'application/json'
)
end
end
end
end