diff --git a/docsite/source/auto-import.html.md b/docsite/source/auto-import.html.md deleted file mode 100644 index 882c5d5f0..000000000 --- a/docsite/source/auto-import.html.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Auto-Import -layout: gem-single -name: dry-system ---- - -After defining a container, we can use its import module that will inject object dependencies automatically. - -Let's say we have an `Application` container and an object that will need a logger: - -``` ruby -# system/import.rb -require 'system/container' -Import = Application.injector - -# In a class definition you simply specify what it needs -# lib/post_publisher.rb -require 'import' -class PostPublisher - include Import['utils.logger'] - - def call(post) - # some stuff - logger.debug("post published: #{post}") - end -end -``` - -### Directory Structure - -You need to provide a specific directory/file structure but names of directories are configurable. The default is as follows: - -``` -#{root} - |- system - |- boot - # arbitrary files that are automatically loaded on finalization -``` - -### Component identifiers - -When components are auto-registered, default identifiers are created based on file paths, ie `lib/api/client` resolves to `API::Client` class with identifier `api.client`. -These identifiers *may have special meaning* where the first name defines its dependency. This is useful for cases where a group of components needs an additional dependency to be always booted for them. - -Let's say we have a group of repository objects that need a `persistence` component to be booted - all we need to do is to follow a simple naming convention: - -- `system/boot/persistence` - here we finalize `persistence` component -- `lib/persistence/user_repo` - here we define components that needs `persistence` - -Here's a sample setup for this scenario: - -``` ruby -# system/container.rb -require 'dry/system/container' - -class Application < Dry::System::Container - configure do |config| - config.name = :app - config.root = Pathname('/my/app') - config.component_dirs.add 'lib' - end -end - -# system/import.rb -require_relative 'container' - -Import = Application.injector - -# system/boot/persistence.rb -Application.boot(:persistence) do |container| - start do - require 'sequel' - container.register('persistence.db', Sequel.connect(ENV['DB_URL'])) - end - - stop do - db.disconnect - end -end - -# lib/persistence/user_repo.rb -require 'import' - -module Persistence - class UserRepo - include Import['persistence.db'] - - def find(conditions) - db[:users].where(conditions) - end - end -end -``` diff --git a/docsite/source/booting.html.md b/docsite/source/booting.html.md deleted file mode 100644 index 89e8ff7ca..000000000 --- a/docsite/source/booting.html.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Booting a Dependency -layout: gem-single -name: dry-system ---- - -In some cases a dependency can be huge, so huge it needs to load some additional files (often 3rd party code) and it may rely on custom configuration. - -Because of this reason dry-system has the concept of booting a dependency. - -The convention is pretty simple. You put files under `system/boot` directory and use your container to register dependencies with the ability to postpone finalization. This gives us a way to define what's needed but load it and boot it on demand. - -Here's a simple example: - -``` ruby -# system/boot/persistence.rb - -Application.boot(:persistence) do - init do - require '3rd_party/db' - end - - start do - register(:database, 3rdParty::Db.new) - end -end -``` - -After defining the finalization block our container will not call it until its own finalization. This means we can require file that defines our container and ask it to boot *just that one :persistence dependency*: - -``` ruby -# system/application/container.rb -class Application < Dry::System::Container - configure do |config| - config.root = Pathname('/my/app') - end -end - -Application.start(:persistence) - -# and now `database` becomes available -Application['database'] -``` - -### Lifecycles - -In some cases, a bootable dependency may have multiple stages of initialization, to support it dry-system provides 3 levels of booting: - -* `init` - basic setup code, here you can require 3rd party code and perform basic configuration -* `start` - code that needs to run for a component to be usable at application's runtime -* `stop` - code that needs to run to stop a component, ie close a database connection, clear some artifacts etc. - -Here's a simple example: - -``` ruby -# system/boot/db.rb - -Application.boot(:db) do - init do - require '3rd_party/db' - - register(:db, 3rdParty::Db.configure(ENV['DB_URL'])) - end - - start do - db.establish_connection - end - - stop do - db.close_connection - end -end -``` - -### Using other bootable dependencies - -It is often needed to use another dependency when booting a component, you can use a convenient `use` API for that, it will auto-boot required dependency -and make it available in the booting context: - -``` ruby -# system/boot/logger.rb -Application.boot(:logger) do - init do - require 'logger' - end - - start do - register(:logger, Logger.new($stdout)) - end -end - -# system/boot/db.rb -Application.boot(:db) do |app| - start do - use :logger - - register(DB.new(ENV['DB_URL'], logger: app[:logger])) - end -end -``` diff --git a/docsite/source/component-providers.html.md b/docsite/source/component-providers.html.md deleted file mode 100644 index 3b3dde02e..000000000 --- a/docsite/source/component-providers.html.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Component Providers -layout: gem-single -name: dry-system ---- - -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`: - -``` ruby -# my_gem -# |- lib/my_gem/components.rb - -Dry::System.register_provider( - :common, - boot_path: Pathname(__dir__).join('boot').realpath() -) -``` - -Then we define our component: - -``` ruby -# 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: - -``` ruby -# 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: - -``` ruby -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: - -``` ruby -# 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: - -``` ruby -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 -``` diff --git a/docsite/source/dependency-auto-injection.html.md b/docsite/source/dependency-auto-injection.html.md new file mode 100644 index 000000000..dbe82df9d --- /dev/null +++ b/docsite/source/dependency-auto-injection.html.md @@ -0,0 +1,82 @@ +--- +title: Dependency auto-injection +layout: gem-single +name: dry-system +--- + +After defining your container, you can use its auto-injector as a mixin to declare a component's dependencies using their container keys. + +For example, if you have an `Application` container and an object that will need a logger: + +``` ruby +# system/import.rb +require "system/container" +Import = Application.injector + +# In a class definition you simply specify what it needs +# lib/post_publisher.rb +require "import" +class PostPublisher + include Import["logger"] + + def call(post) + # some stuff + logger.debug("post published: #{post}") + end +end +``` + +### Auto-registered component keys + +When components are auto-registered, their default keys are based on their file paths and your [component dir](/gems/dry-system/component-dirs) configuration. For example, `lib/api/client.rb` will have the key `"api.client"` and will resolve an instance of `API::Client`. + +Resolving a component will also start a registered [provider](/gems/dry-system/providers) if it shares the same name as the root segment of its container key. This is useful in cases where a group of components require an additional dependency to be always made available. + +For example, if you have a group of repository objects that need a `persistence` provider to be started, all you need to do is to follow this naming convention: + +- `system/providers/persistence.rb` - where you register your `:persistence` provider +- `lib/persistence/user_repo` - where you can define any components that need the components or setup established by the `persistence` provider + +Here's a sample setup for this scenario: + +``` ruby +# system/container.rb +require "dry/system/container" + +class Application < Dry::System::Container + configure do |config| + config.root = Pathname("/my/app") + config.component_dirs.add "lib" + end +end + +# system/import.rb +require_relative "container" + +Import = Application.injector + +# system/providers/persistence.rb +Application.register_provider(:persistence) do + start do + require "sequel" + container.register("persistence.db", Sequel.connect(ENV['DB_URL'])) + end + + stop do + container["persistence.db"].disconnect + end +end + +# lib/persistence/user_repo.rb +require "import" + +module Persistence + class UserRepo + include Import["persistence.db"] + + def find(conditions) + db[:users].where(conditions) + end + end +end +``` diff --git a/docsite/source/external-provider-sources.md b/docsite/source/external-provider-sources.md new file mode 100644 index 000000000..7485cc14c --- /dev/null +++ b/docsite/source/external-provider-sources.md @@ -0,0 +1,123 @@ +--- +title: External provider sources +layout: gem-single +name: dry-system +--- + +You can distribute your own components to other dry-system users via external provider sources, which can be used as the basis for providers within any dry-system container. + +Provider sources look and work the same as regular providers, which means allowing you to use their full lifecycle for creating, configuring, and registering your components. + +To distribute a group of provider sources (defined in their own files), register them with `Dry::System`: + +``` ruby +# my_gem +# |- lib/my_gem/provider_sources.rb + +Dry::System.register_provider_sources(:common, boot_path: File.join(__dir__, "provider_sources")) +``` + +Then, define your provider source: + +``` ruby +# my_gem +# |- lib/my_gem/provider_sources/exception_notifier.rb + +Dry::System.register_provider_source(:exception_notifier, group: :my_gem) do + init do + require "some_exception_notifier" + end + + start do + register(:exception_notifier, SomeExceptionNotifier.new) + end +end +``` + +Then you can use this provider source when you register a provider in a dry-system container: + +``` ruby +# system/app/container.rb + +require "dry/system/container" +require "my_gem/provider_sources" + +module App + class Container < Dry::System::Container + register_provider(:exception_notifier, from: :my_gem) + end +end + +App::Container[:exception_notifier] +``` + +### Customizing provider sources + +You can customize a provider source for your application via `before` and `after` callbacks for its lifecycle steps. + +For example, you can register additional components based on the provider source's own registrations via an `after(:start)` callback: + +``` ruby +module App + class Container < Dry::System::Container + register_provider(:exception_notifier, from: :my_gem) do + after(:start) + register(:my_notifier, container[:exception_notifier]) + end + end + end +end +``` + +The following callbacks are supported: + +- `before(:prepare)` +- `after(:prepare)` +- `before(:start)` +- `after(:start)` + +### Providing component configuration + +Provider sources can define their own settings using [dry-configurable’s](/gems/dry-configurable) `setting` API. These will be configured when the provider source is used by a provider. The other lifecycle steps in the provider souce can access the configured settings as `config`. + +For example, here’s an extended `:exception_notifier` provider source with settings: + +``` ruby +# my_gem +# |- lib/my_gem/provider_sources/exception_notifier.rb + +Dry::System.register_component(:exception_notifier, provider: :common) do + setting :environments, default: :production, constructor: Types::Strict::Array.of(Types::Strict::Symbol) + setting :logger + + 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 +``` + +This defines two settings: + +- `: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 + +To configure this provider source, you can use a `configure` block when defining your provider using the source: + +``` ruby +module App + class Container < Dry::System::Container + register_provider(:exception_notifier, from: :my_gem) do + require "logger" + + configure do |config| + config.logger = Logger.new($stdout) + end + end + end +end +``` diff --git a/docsite/source/index.html.md b/docsite/source/index.html.md index 9c8d1eecb..f7706ab38 100644 --- a/docsite/source/index.html.md +++ b/docsite/source/index.html.md @@ -6,10 +6,10 @@ type: gem sections: - container - component-dirs - - booting - - auto-import - - component-providers + - providers + - dependency-auto-injection - plugins + - external-provider-sources - settings - test-mode --- @@ -21,8 +21,7 @@ This library relies on very basic mechanisms provided by Ruby, specifically `req It does a couple of things for you: * Provides an abstract dependency container implementation -* Handles `$LOAD_PATH` configuration -* Loads needed files using `require` +* Integrates with an autoloader, or handles `$LOAD_PATH` for you and loads needed files using `require` * Resolves object dependencies automatically * Supports auto-registration of dependencies via file/dir naming conventions * Supports multi-system setups (ie your application is split into multiple sub-systems) diff --git a/docsite/source/plugins.html.md b/docsite/source/plugins.html.md index 98dfcee7d..ce863443d 100644 --- a/docsite/source/plugins.html.md +++ b/docsite/source/plugins.html.md @@ -4,27 +4,11 @@ layout: gem-single name: dry-system --- -Dry-system has already built-in plugins that you can enable, and it’s very easy to write your own. - -## Logging support - -You can now enable a default system logger by simply enabling `:logging` plugin, you can also configure log dir, level and provide your own logger class. - -```ruby -class App < Dry::System::Container - use :logging -end - -# default logger is registered as a standard object, so you can inject it via auto-injection -App[:logger] - -# short-cut method is provided too, which is convenient in some cases -App.logger -``` +dry-system has already built-in plugins that you can enable, and it’s very easy to write your own. ## Zeitwerk -With `:zeitwerk` plugin you can easily use [Zeitwerk](https://github.com/fxn/zeitwerk) as your applications's code loader: +With the `:zeitwerk` plugin you can easily use [Zeitwerk](https://github.com/fxn/zeitwerk) as your applications's code loader: > Given a conventional file structure, Zeitwerk is able to load your project's classes and modules on demand (autoloading), or upfront (eager loading). You don't need to write require calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants. (Zeitwerk docs) @@ -93,58 +77,74 @@ If you need to adjust the Zeitwerk configuration, you can do so by accessing the MyContainer.autoloader.ignore("./some_path.rb) ``` -## Monitoring +## Application environment -Another plugin is called `:monitoring` which allows you to enable object monitoring, which is built on top of dry-monitor’s instrumentation API. Let’s say you have an object registered under `"users.operations.create",` and you’d like to add additional logging: +You can use the `:env` plugin to set and configure an `env` setting for your application. ```ruby class App < Dry::System::Container - use :logging - use :monitoring + use :env + + configure do |config| + config.env = :staging + end end +``` -App.monitor("users.operations.create") do |event| - App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" +You can provide environment inferrer, which is probably something you want to do, here’s how dry-web sets up its environment: + +```ruby +module Dry + module Web + class Container < Dry::System::Container + use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } + end + end end ``` -You can also provide specific methods that should be monitored, let’s say we’re only interested in `#call` method: +## Logging + +You can now enable a default system logger by simply enabling `:logging` plugin, you can also configure log dir, level and provide your own logger class. ```ruby -App.monitor("users.operations.create", methods: %i[call]) do |event| - App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" +class App < Dry::System::Container + use :logging end + +# default logger is registered as a standard object, so you can inject it via auto-injection +App[:logger] + +# short-cut method is provided too, which is convenient in some cases +App.logger ``` -## Setting environment +## Monitoring -Environment can now be set in a non-web systems too. Previously this was only possible in dry-web, now any ruby app based on dry-system can use this configuration setting via `:env` plugin: +Another plugin is called `:monitoring` which allows you to enable object monitoring, which is built on top of dry-monitor’s instrumentation API. Let’s say you have an object registered under `"users.operations.create",` and you’d like to add additional logging: ```ruby class App < Dry::System::Container - use :env + use :logging + use :monitoring +end - configure do |config| - config.env = :staging - end +App.monitor("users.operations.create") do |event| + App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" end ``` -You can provide environment inferrer, which is probably something you want to do, here’s how dry-web sets up its environment: +You can also provide specific methods that should be monitored, let’s say we’re only interested in `#call` method: ```ruby -module Dry - module Web - class Container < Dry::System::Container - use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } - end - end +App.monitor("users.operations.create", methods: %i[call]) do |event| + App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" end ``` ## Experimental bootsnap support -dry-system is already pretty fast, but in a really big apps, it can take over 2 seconds to boot. You can now speed it up significantly by using `:bootsnap` plugin, which simply configures bootsnap for you: +dry-system is already pretty fast, but in a really big apps, it can take some seconds to boot. You can now speed it up significantly by using `:bootsnap` plugin, which simply configures bootsnap for you: ```ruby class App < Dry::System::Container diff --git a/docsite/source/providers.html.md b/docsite/source/providers.html.md new file mode 100644 index 000000000..322885b17 --- /dev/null +++ b/docsite/source/providers.html.md @@ -0,0 +1,97 @@ +--- +title: Providers +layout: gem-single +name: dry-system +--- + +Some components can be large, stateful, or requiring specific configuration as part of their setup (such as when dealing with third party code). You can use providers to manage and register these components across several distinct lifecycle steps. + +You can define your providers as individual source files in `system/providers/`, for example: + +``` ruby +# system/providers/persistence.rb + +Application.register_provider(:database) do + prepare do + require "3rd_party/db" + end + + start do + register(:database, 3rdParty::DB.new) + end +end +``` + +The provider’s lifecycle steps will not run until the provider is required by another component, is started directly, or when the container finalizes. + +This means you can require your container and ask it to start just that one provider: + +``` ruby +# system/application/container.rb +class Application < Dry::System::Container + configure do |config| + config.root = Pathname("/my/app") + end +end + +Application.start(:database) + +# and now `database` becomes available +Application["database"] +``` + +### Provider lifecycle + +The provider lifecycle consists of three steps, each with a distinct purpose: + +* `prepare` - basic setup code, here you can require 3rd party code and perform basic configuration +* `start` - code that needs to run for a component to be usable at application's runtime +* `stop` - code that needs to run to stop a component, ie close a database connection, clear some artifacts etc. + +Here's a simple example: + +``` ruby +# system/providers/db.rb + +Application.register_provider(:database) do + init do + require '3rd_party/db' + + register(:database, 3rdParty::DB.configure(ENV['DB_URL'])) + end + + start do + container[:database].establish_connection + end + + stop do + container[:database].close_connection + end +end +``` + +### Using other providers + +You can start one provider as a dependency of another by invoking the provider’s lifecycle directly on the `target` container (i.e. your application container): + +``` ruby +# system/providers/logger.rb +Application.register_provider(:logger) do + init do + require "logger" + end + + start do + register(:logger, Logger.new($stdout)) + end +end + +# system/providers/db.rb +Application.register_provider(:db) do + start do + target.start :logger + + register(DB.new(ENV['DB_URL'], logger: target[:logger])) + end +end +``` diff --git a/docsite/source/settings.html.md b/docsite/source/settings.html.md index 0ddaaf2cd..e5c693240 100644 --- a/docsite/source/settings.html.md +++ b/docsite/source/settings.html.md @@ -6,69 +6,62 @@ name: dry-system ## Basic usage -dry-system provides a built-in `:settings` component which you can use in your application. In order to set it up, simply define a bootable `:settings` component and import it from the `:system` provider: +dry-system provides a `:settings` provider source that you can use to load settings and share them throughout your application. To use this provider source, create your own `:settings` provider using the provider source from `:dry_system`, then declare your settings inside `settings` block (using [dry-configurable’s](/gems/dry-configurable) `setting` API): ```ruby -# in system/boot/settings.rb -require "dry/system/components" -require "path/to/dry/types/file" +# system/providers/settings.rb: + +require "dry/system/provider_sources" + +Application.register_provider(:settings, from: :dry_system) do + before :prepare do + # Change this to load your own `Types` module if you want type-checked settings + require "your/types/module" + end -Application.boot(:settings, from: :system) do settings do - key :database_url, Types::String.constrained(filled: true) - key :logger_level, Types::Symbol.constructor { |value| value.to_s.downcase.to_sym } - .default(:info) - .enum(:trace, :unknown, :error, :fatal, :warn, :info, :debug) + setting :database_url, constructor: Types::String.constrained(filled: true) + + setting :logger_level, default: :info, constructor: Types::Symbol + .constructor { |value| value.to_s.downcase.to_sym } + .enum(:trace, :unknown, :error, :fatal, :warn, :info, :debug) end end ``` -Now, dry-system will map values from `ENV` variable to `settings` struct and allows you to use it in the application: +Your provider will then map `ENV` variables to a struct object giving access to your settings as their own methods, which you can use throughout your application: ```ruby -Application[:settings] # => dry-struct object with settings +Application[:settings].database_url # => "postgres://..." +Application[:settings].logger_level # => :info ``` -You can also use settings object in other bootable dependencies: +You can use this settings object in other providers: ```ruby -Application.boot(:redis) do |container| - init do +Application.register_provider(:redis) do + start do use :settings - uri = URI.parse(container[:settings].redis_url) + uri = URI.parse(target[:settings].redis_url) redis = Redis.new(host: uri.host, port: uri.port, password: uri.password) - container.register('persistance.redis', redis) + register('persistance.redis', redis) end end ``` -Or use `:settings` as an injectible dependency in your classes: +Or as an injected dependency in your classes: ```ruby module Operations class CreateUser - include Import[:settings, :repository] - - def call(id:) - settings # => dry-struct object with settings + include Import[:settings] - # ... + def call(params) + settings # => your settings struct end end end end ``` - -## Default values - -You can [use dry-types](https://dry-rb.org/gems/dry-types/master/default-values/) for provide default value for specific setting: - -```ruby -settings do - key :redis_url, Types::Coercible::String.default('') -end -``` - -In this case, if you don't have `ENV['REDIS_URL']` value, you get `''` as the default value for `settings.redis_url` calls.