Application framework with state management and built-in dependency injection support
Dry::Core::Container
(via 2b76554e5925fc92614627d5c1e0a9177cecf12f) (@solnic)This is a major overhaul of bootable components (now known as “Providers”), and brings major advancements to other areas, including container imports and exports.
Deprecations are in place for otherwise breaking changes to commonly used parts of dry-system, though some breaking changes remain.
This prepares the way for dry-system 1.0, which will be released in the coming months.
Containers can configure specific components for export using config.exports
(@timriley in #209).
class MyContainer < Dry::System::Container
configure do |config|
config.exports = %w[component_a component_b]
end
end
Containers importing another container with configured exports will import only those components.
When importing a specific set of components (see the note in the “Changed” section below), only those components whose keys intersect with the configured exports will be imported.
A :zeitwerk
plugin, to set up Zeitwerk and integrate it with your container configuration (@ianks and @timriley in #197, #222, 13f8c87, #223)
This makes it possible to enable Zeitwerk with a one-liner:
class MyContainer < Dry::System::Container
use :zeitwerk
configure do |config|
config.component_dirs.add "lib"
# ...
end
end
The plugin makes a Zeitwerk::Loader
instance available at config.autoloader
, and then in an after-:configure
hook, the plugin will set up the loader to work with all of your configured component dirs and their namespaces. It will also enable the Dry::System::Loader::Autoloading
loader for all component dirs, plus disable those dirs from being added to the $LOAD_PATH
.
The plugin accepts the following options:
loader:
- (optional) to use a pre-initialized loader, if required.run_setup:
- (optional) a bool to determine whether to run Zeitwerk::Loader#setup
as part of the after-:configure
hook. This may be useful to disable in advanced cases when integrating with an externally managed loader.eager_load:
- (optional) a bool to determine whether to run Zeitwerk::Loader#eager_load
as part of an after-:finalize
hook. When not provided, it will default to true if the :env
plugin is enabled and the env is set to :production
.debug:
- (optional) a bool to set whether Zeitwerk should log to $stdout
.New Identifier#end_with?
and Identifier#include?
predicates (@timriley in #219)
These are key segment-aware predicates that can be useful when checking components as part of container configuration.
identifier.key # => "articles.operations.create"
identifier.end_with?("create") # => true
identifier.end_with?("operations.create") # => true
identifier.end_with?("ate") # => false, not a whole segment
identifier.end_with?("nope") # => false, not part of the key at all
identifier.include?("operations") # => true
identifier.include?("articles.operations") # => true
identifier.include?("operations.create") # => true
identifier.include?("article") # false, not a whole segment
identifier.include?("update") # => false, not part of the key at all
An instance
setting for component dirs allows simpler per-dir control over component instantiation (@timriley in #215)
This optional setting should be provided a proc that receives a single Dry::System::Component
instance as an argument, and should return the instance for the given component.
configure do |config|
config.component_dirs.add "lib" do |dir|
dir.instance = proc do |component|
if component.identifier.include?("workers")
# Register classes for jobs
component.loader.constant(component)
else
# Otherwise register regular instances per default loader
component.loader.call(component)
end
end
end
end
For complete control of component loading, you should continue to configure the component dir’s loader
instead.
A new ComponentNotLoadableError
error and helpful message is raised when resolving a component and an unexpected class is defined in the component’s source file (@cllns in #217).
The error shows expected and found class names, and inflector configuration that may be required in the case of class names containing acronyms.
Registrations made in providers (by calling register
inside a provider step) have all their registration options preserved (such as a block-based registration, or the memoize:
option) when having their registration merged into the target container after the provider lifecycle steps complete (@timriley in #212).
Providers can no longer implicitly re-start themselves while in the process of starting and cause an infinite loop (@timriley #213).
This was possible before when a provider resolved a component from the target container that auto-injected dependencies with container keys sharing the same base key as the provider name.
“Bootable components” (also referred to in some places simply as “components”) have been renamed to “Providers” (@timriley in #200).
Register a provider with Dry::System::Container.register_provider
(Dry::System::Container.boot
has been deprecated):
MyContainer.register_provider(:mailer) do
# ...
end
Provider init
lifecycle step has been deprecated and renamed to prepare
(@timriley in #200).
MyContainer.reigster_provider(:mailer) do
# Rename `init` to `prepare`
prepare do
require "some/third_party/mailer"
end
end
Provider behavior is now backed by a class per provider, known as the “Provider source” (@timriley in #202).
The provider source class is created for each provider as a subclass of Dry::System::Provider::Source
.
You can still register simple providers using the block-based DSL, but the class backing means you can share state between provider steps using regular instance variables:
MyContainer.reigster_provider(:mailer) do
prepare do
require "some/third_party/mailer"
@some_config = ThirdParty::Mailer::Config.new
end
start do
# Since the `prepare` step will always run before start, we can access
# @some_config here
register "mailer", ThirdParty::Mailer.new(@some_config)
end
end
Inside this register_provider
block, self
is the source subclass itself, and inside each of the step blocks (i.e. prepare do
), self
will be the instance of that provider source.
For more complex providers, you can define your own source subclass and register it directly with the source:
option for register_provider
. This allows you to more readily use standard arrangements for factoring your logic within a class, such as extraction to another method:
MyContainer.register_provider(:mailer, source: Class.new(Dry::System::Provider::Source) {
# The provider lifecycle steps are ordinary methods
def prepare
end
def start
mailer = some_complex_logic_to_build_the_mailer(some: "config")
register(:mailer, mailer)
end
private
def some_complex_logic_to_build_the_mailer(**options)
# ...
end
})
The block argument to Dry::System::Container.register_provider
(previously .boot
) has been deprecated. (@timriley in #202).
This argument was used to give you access to the provider's target container (i.e. the container on which you were registering the provider).
To access the target container, you can use #target_container
(or #target
as a convenience alias) instead.
You can also access the provider's own container (which is where the provider's components are registered when you call register
directly inside a provider step) as #provider_container
(or #container
as a convenience alias).
use(provider_name)
inside a provider step has been deprecated. Use target_container.start(provider_name)
instead (@timriley in #211 and #224)
Now that you can access target_container
consistently within all provider steps, you can use it to also start any other providers as you require without any special additional method. This also allows you to invoke other provider lifecycle steps, like target_container.prepare(provider_name)
.
method_missing
-based delegation within providers to target container registrations has been removed (BREAKING) (@timriley in #202)
Delegation to registrations with the provider's own container has been kept, since it can be a convenient way to access registrations made in a prior lifecycle step:
MyContainer.register_provider(:mailer, namespace: true) do
prepare do
register :config, "mailer config here"
end
start do
config # => "mailer config here"
end
end
The previous "external component" and "provider" concepts have been renamed to "external provider sources", in keeping with the new provider terminology outlined above (@timriley in #200 and #202).
You can register a collection of external provider sources defined in their own source files via Dry::System.register_provider_sources
(Dry::System.register_provider
has been deprecated):
require "dry/system"
Dry::System.register_provider_sources(path)
You can register an individual external provider source via Dry::System.register_provider_source
(Dry::System.register_component
has been deprecated):
Dry::System.register_provider_source(:something, group: :my_gem) do
start do
# ...
end
end
Just like providers, you can also register a class as an external provider source:
module MyGem
class MySource < Dry::System::Provider::Source
def start
# ...
end
end
end
Dry::System.register_provider_source(:something, group: :my_gem, source: MyGem::MySource)
The group:
argument when registering an external provider sources is for preventing name clashes between provider sources. You should use an underscored version of your gem name or namespace when registering your own provider sources.
Registering a provider using an explicitly named external provider source via key:
argument is deprecated, use the source:
argument instead (@timriley in #202).
You can register a provider using the same name as an external provider source by specifying the from:
argument only, as before:
# Elsewhere
Dry::System.register_provider_source(:something, group: :my_gem) { ... }
# In your app:
MyContainer.register_provider(:something, from: :my_gem)
When you wish the name your provider differently, this is when you need to use the source:
argument:
MyContainer.register_provider(:differently_named, from: :my_gem, source: :something)
When you're registering a provider using an external provider source, you cannot provie your own Dry::System::Provider::Source
subclass as the source:
, since that source class is being provided by the external provider source.
Provider source settings are now defined using dry-configurable’s setting
API at the top-level scope (@timriley in #202).
Use the top-level setting
method to define your settings (the settings
block and settings defined inside the block using key
is deprecated). Inside the provider steps, the configured settings can be accessed as config
:
# In the external provider source
Dry::System.register_provider_source(:something, group: :my_gem) do
setting :my_option
start do
# Do something with `config.my_option` here
end
end
When using an external provider source, configure the source via the #configure
:
# In your application's provider using the external source
MyContainer.register_provider(:something, from: :my_gem) do
configure do |config|
config.my_option = "some value"
end
end
To provide default values and type checking or constraints for your settings, use the dry-configurable’s default:
and constructor:
arguments:
# Constructor can take any proc being passed the provided value
setting :my_option, default: "hello", constructor: -> (v) { v.to_s.upcase }
# Constructor will also work with dry-types objects
setting :my_option, default: "hello", constructor: Types::String.constrained(min_size: 3)
External provider sources can define their own methods for use by the providers alongside lifecycle steps (@timriley in #202).
Now that provider sources are class-backed, external provider sources can define their own methods to be made available when that provider source is used. This makes it possible to define your own extended API for interacting with the provider source:
# In the external provider source
module MyGem
class MySource < Dry::System::Provider::Source
# Standard lifecycle steps
def start
# Do something with @on_start here
end
# Custom behavior available when this provider source is used in a provider
def on_start(&block)
@on_start = block
end
end
end
Dry::System.register_provider_source(:something, group: :my_gem, source: MyGem::MySource)
# In your application's provider using the external source
MyContainer.register_provider(:something, from: :my_gem) do
# Use the custom method!
on_start do
# ...
end
end
Providers can be registered conditionally using the if:
option (@timriley in #218).
You should provide a simple truthy or falsey value to if:
, and in the case of falsey value, the provider will not be registered.
This is useful in cases where you have providers that are loaded explicitly for specific runtime configurations of your app (e.g. when they are needed for specific tasks or processes only), but you do not need them for your primaary app process, for which you may finalize your container.
bootable_dirs
container setting has been deprecated and replaced by provider_dirs
(@timriley in #200).
The default value for provider_dirs
is now "system/providers
".
Removed the unused system_dir
container setting (BREAKING) (@timriley in #200)
If you’ve configured this inside your container, you can remove it.
dry-system’s first-party external provider sources now available via require "dry/system/provider_sources"
, with the previous require "dry/system/components"
deprecated (@timriley in #202).
When using registering a provider using a first-party dry-system provider source, from: :dry_system
instead of from: :system
(which is now deprecated) (@timriley in #202).
MyContainer.register_provider(:settings, from: :dry_system) do
# ...
end
When registering a provider using the :settings
provider source, settings are now defined using setting
inside a settings
block, rather than key
, which is deprecated (@timriley in #202).
This setting
method uses the dry-configurable setting API:
MyContainer.register_provider(:settings, from: :dry_system) do
settings do
# Previously:
# key :my_int_setting, MyTypes::Coercible::Integer
# Now:
setting :my_setting, default: 0, constructor: MyTypes::Coercible::Integer
end
end
The :settings
provider source now requires the dotenv gem to load settings from .env*
files (BREAKING) (@timriley in #204)
To ensure you can load your settings from these .env*
files, add gem "dotenv"
to your Gemfile
.
Dry::System::Container
can be now be configured direclty using the setting writer methods on the class-level .config
object, without going the .configure(&block)
API (@timriley in #207).
If configuring via the class-level .config
object, you should call .configured!
after you're completed your configuration, which will finalize (freeze) the config
object and then run any after-:configure
hooks.
Dry::System::Container.configure(&block)
will now finalize (freeze) the config
object by default, before returning (@timriley in #207).
You can opt out of this behavior by passing the finalize_config: false
option:
class MyContainer < Dry::System::Container
configure(finalize_config: false) do |config|
# ...
end
# `config` is still non-finalized here
end
Dry::System::Container.finalize!
will call .configured!
(if it has not yet been called) before doing its work (@timriley in #207)
This ensures config finalization is an intrinsic part of the overall container finalization process.
The Dry::System::Container
before(:configure)
hook has been removed (BREAKING) (@timriley in #207).
This was previously used for plugins to register their own settings, but this was not necessary given that plugins are modules, and can use their ordinary .extended(container_class)
hook to register their settings. Essentially, any time after container subclass definition is "before configure" in nature.
Container plugins should define their settings on the container using their module .extended
hook, no longer in a before(:configure)
hook (as above) (BREAKING) (@timriley in #207).
This ensures the plugin settings are available immediately after you’ve enabled the plugin via Dry::System::Container.use
.
The Dry::System::Container
key namespace_separator
setting is no longer expected to be user-configured. A key namespace separator of "." is hard-coded and expected to remain the separator string. (@timriley in #206)
Containers can import a specific subset of another container’s components via changes to .import
, which is now .import(keys: nil, from:, as:)
(with prior API deprecated) (@timriley in #209)
To import specific components:
class MyContainer < Dry::System::Container
# config, etc.
# Will import components with keys "other.component_a", "other.component_b"
import(
keys: %w[component_a component_b],
from: OtherContainer,
as: :other
)
Omitting keys:
will import all the components available from the other container.
Components imported into a container from another will be protected from subsequent export unless explicitly configured in config.exports
(@timriley in #209)
Imported components are considered “private” by default because they did not originate in container that imported them.
This ensures there are no redundant imports in arrangements where multiple all containers import a common “base” container, and then some of those containers then import each other.
Container imports are now made without finalizing the exporting container in most cases, ensuring more efficient imports (@timriley in #209)
Now, the only time the exporting container will be finalized is when a container is importing all components, and the exporting container has not declared any components in config.exports
.
[Internal] The manual_registrar
container setting and associated ManualRegistrar
class have been renamed to manifest_registrar
and ManifestRegistrar
respectively (BREAKING) (@timriley in #208).
The default value for the container registrations_dir
setting has been changed from "container"
to "system/registrations"
(BREAKING) (@timriley in #208)
The :dependency_graph
plugin now supports all dry-auto_inject injector strategies (@davydovanton and @timriley in #214)