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

octocatEdit on GitHub