Component Providers

External dependencies can be provided as bootable components, these components can be shared across applications with the ability to configure them and customize booting process. This is especially useful in situations where you have a set of applications with many common components.

Bootable components are handled by component providers, which can register themselves and set up their components. Let's say we want to provide a common exception notifier for many applications. First, we register our provider called :common:

# my_gem
#  |- lib/my_gem/components.rb

Dry::System.register_provider(
  :common,
  boot_path: Pathname(__dir__).join('boot').realpath()
)

Then we define our component:

# my_gem
#  |- lib/my_gem/boot/exception_notifier.rb
Dry::System.register_component(:exception_notifier, provider: :common) do
  init do
    require "some_exception_notifier"
  end

  start do
    register(:exception_notifier, SomeExceptionNotifier.new)
  end
end

Now in application container we can easily boot this external component:

# system/app/container.rb
require "dry/system/container"
require "my_gem/components"

module App
  class Container < Dry::System::Container
    boot(:exception_notifier, from: :common)
  end
end

App::Container[:exception_notifier]

Hooking into booting process

You can use lifecycle before/after callbacks if you need to do something special. For instance, you may want to customize object registration, for this you can use after(:start) callback, which receives a container that was set up by your :common component provider:

module App
  class Container < Dry::System::Container
    boot(:exception_notifier, from: :common) do
      after(:start) do |common|
        register(:notifier, common[:exception_notifier])
      end
    end
  end
end

Following callbacks are supported:

  • before(:init)
  • after(:init)
  • before(:start)
  • after(:start)

Providing component configuration

Components can specify their configuration settings using settings block, settings specify keys and types, and default values can be set too. If a component uses settings, then lifecycle steps have access to its config.

Here's an extended :exception_notifier example which uses its own settings:

# my_gem
#  |- lib/my_gem/boot/exception_notifier.rb
Dry::System.register_component(:exception_notifier, provider: :common) do
  settings do
    key :environments, Types::Strict::Array.of(Types::Strict::Symbol).default(%i[production])
    key :logger, Types::Any
  end

  init do
    require "some_exception_notifier"
  end

  start do
    # now we have access to `config`
    register(:exception_notifier, SomeExceptionNotifier.new(config.to_h))
  end
end

In this example we define two config keys:

  • :environments which is a list of environment identifiers with default value set to [:production]
  • :logger an object that should be used as the logger, which must be configured

In order to configure our :logger we simply use configure block when registering the component:

module App
  class Container < Dry::System::Container
    boot(:exception_notifier, from: :common) do
      after(:init) do
        require "logger"
      end

      configure do |config|
        config.logger = Logger.new($stdout)
      end
    end
  end
end

octocatEdit on GitHub