Class: Dry::System::Container

Inherits:
Object
  • Object
show all
Extended by:
Configurable, Container::Mixin, Plugins
Defined in:
lib/dry/system/container.rb,
lib/dry/system/stubs.rb

Overview

Abstract container class to inherit from

Container class is treated as a global registry with all system components. Container can also import dependencies from other containers, which is useful in complex systems that are split into sub-systems.

Container can be finalized, which triggers loading of all the defined components within a system, after finalization it becomes frozen. This typically happens in cases like booting a web application.

Before finalization, Container can lazy-load components on demand. A component can be a simple class defined in a single file, or a complex component which has init/start/stop lifecycle, and it’s defined in a boot file. Components which specify their dependencies using Import module can be safely required in complete isolation, and Container will resolve and load these dependencies automatically.

Furthermore, Container supports auto-registering components based on dir/file naming conventions. This reduces a lot of boilerplate code as all you have to do is to put your classes under configured directories and their instances will be automatically registered within a container.

Every container needs to be configured with following settings:

  • ‘:name` - a unique container identifier

  • ‘:root` - a system root directory (defaults to `pwd`)

  • ‘:system_dir` - directory name relative to root, where bootable components

    can be defined in `boot` dir this defaults to `system`
    

Examples:

class MyApp < Dry::System::Container
  configure do |config|
    config.name = :my_app

    # this will auto-register classes from 'lib/components'. ie if you add
    # `lib/components/repo.rb` which defines `Repo` class, then it's
    # instance will be automatically available as `MyApp['repo']`
    config.auto_register = %w(lib/components)
  end

  # this will configure $LOAD_PATH to include your `lib` dir
  load_paths!('lib')
end

Defined Under Namespace

Modules: Stubs

Class Method Summary collapse

Methods included from Plugins

enabled_plugins, inherited, loaded_dependencies, register, registry, use

Class Method Details

.after(event, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



607
608
609
# File 'lib/dry/system/container.rb', line 607

def after(event, &block)
  hooks[event] << block
end

.auto_register!(dir) { ... } ⇒ self

Auto-registers components from the provided directory

Typically you want to configure auto_register directories, and it will work automatically. Use this method in cases where you want to have an explicit way where some components are auto-registered, or if you want to exclude some components from being auto-registered

Examples:

class MyApp < Dry::System::Container
  configure do |config|
    # ...
  end

  # with a dir
  auto_register!('lib/core')

  # with a dir and a custom registration block
  auto_register!('lib/core') do |config|
    config.instance do |component|
      # custom way of initializing a component
    end

    config.exclude do |component|
      # return true to exclude component from auto-registration
    end
  end
end

Parameters:

  • dir (String)

    The dir name relative to the root dir

Yields:

  • AutoRegistrar::Configuration

Returns:

  • (self)

See Also:



432
433
434
435
# File 'lib/dry/system/container.rb', line 432

def auto_register!(dir, &block)
  auto_registrar.(dir, &block)
  self
end

.auto_registrarObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



527
528
529
# File 'lib/dry/system/container.rb', line 527

def auto_registrar
  @auto_registrar ||= config.auto_registrar.new(self)
end

.boot(name, opts = {}, &block) ⇒ self

Registers finalization function for a bootable component

By convention, boot files for components should be placed in ‘%system_dir/boot` and they will be loaded on demand when components are loaded in isolation, or during finalization process.

Examples:

# system/container.rb
class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :core
    config.auto_register = %w(lib/apis lib/core)
  end

# system/boot/db.rb
#
# Simple component registration
MyApp.boot(:db) do |container|
  require 'db'

  container.register(:db, DB.new)
end

# system/boot/db.rb
#
# Component registration with lifecycle triggers
MyApp.boot(:db) do |container|
  init do
    require 'db'
    DB.configure(ENV['DB_URL'])
    container.register(:db, DB.new)
  end

  start do
    db.establish_connection
  end

  stop do
    db.close_connection
  end
end

# system/boot/db.rb
#
# Component registration which uses another bootable component
MyApp.boot(:db) do |container|
  use :logger

  start do
    require 'db'
    DB.configure(ENV['DB_URL'], logger: logger)
    container.register(:db, DB.new)
  end
end

# system/boot/db.rb
#
# Component registration under a namespace. This will register the
# db object under `persistence.db` key
MyApp.namespace(:persistence) do |persistence|
  require 'db'
  DB.configure(ENV['DB_URL'], logger: logger)
  persistence.register(:db, DB.new)
end

Parameters:

  • name (Symbol)

    a unique identifier for a bootable component

Returns:

  • (self)

See Also:



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/dry/system/container.rb', line 219

def boot(name, opts = {}, &block)
  if components.key?(name)
    raise DuplicatedComponentKeyError, "Bootable component #{name.inspect} was already registered"
  end

  component =
    if opts[:from]
      boot_external(name, opts, &block)
    else
      boot_local(name, opts, &block)
    end
  self

  components[name] = component
end

.boot_external(identifier, from:, key: nil, namespace: nil, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



237
238
239
240
241
242
243
244
245
# File 'lib/dry/system/container.rb', line 237

def boot_external(identifier, from:, key: nil, namespace: nil, &block)
  component = System.providers[from].component(
    identifier, key: key, namespace: namespace, finalize: block, container: self
  )

  booter.register_component(component)

  component
end

.boot_local(identifier, namespace: nil, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



248
249
250
251
252
253
254
# File 'lib/dry/system/container.rb', line 248

def boot_local(identifier, namespace: nil, &block)
  component = Components::Bootable.new(identifier, container: self, namespace: namespace, &block)

  booter.register_component(component)

  component
end

.boot_pathObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



522
523
524
# File 'lib/dry/system/container.rb', line 522

def boot_path
  root.join("#{config.system_dir}/boot")
end

.booterObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



517
518
519
# File 'lib/dry/system/container.rb', line 517

def booter
  @booter ||= config.booter.new(boot_path)
end

.component(identifier, **options) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



542
543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'lib/dry/system/container.rb', line 542

def component(identifier, **options)
  if (component = booter.components.detect { |c| c.identifier == identifier })
    component
  else
    Component.new(
      identifier,
      loader: config.loader,
      namespace: config.default_namespace,
      separator: config.namespace_separator,
      inflector: config.inflector,
      **options,
    )
  end
end

.configure(&block) ⇒ self

Configures the container

Examples:

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :my_app
    config.auto_register = %w(lib/apis lib/core)
  end
end

Returns:

  • (self)


104
105
106
107
108
109
# File 'lib/dry/system/container.rb', line 104

def configure(&block)
  super(&block)
  load_paths!(config.system_dir)
  hooks[:configure].each { |hook| instance_eval(&hook) }
  self
end

.enable_stubs!Object

Enables stubbing container’s components

Examples:

require 'dry/system/stubs'

MyContainer.enable_stubs!
MyContainer.finalize!

MyContainer.stub('some.component', some_stub_object)

Returns:

  • Container



26
27
28
29
30
# File 'lib/dry/system/stubs.rb', line 26

def self.enable_stubs!
  super
  extend ::Dry::System::Container::Stubs
  self
end

.finalize!(freeze: true) {|_self| ... } ⇒ self

Finalizes the container

This triggers importing components from other containers, booting registered components and auto-registering components. It should be called only in places where you want to finalize your system as a whole, ie when booting a web application

Examples:

# system/container.rb
class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.name = :my_app
    config.auto_register = %w(lib/apis lib/core)
  end
end

# You can put finalization file anywhere you want, ie system/boot.rb
MyApp.finalize!

# If you need last-moment adjustments just before the finalization
# you can pass a block and do it there
MyApp.finalize! do |container|
  # stuff that only needs to happen for finalization
end

Yields:

  • (_self)

Yield Parameters:

Returns:

  • (self)

    frozen container



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/dry/system/container.rb', line 294

def finalize!(freeze: true, &block)
  return self if finalized?

  yield(self) if block

  importer.finalize!
  booter.finalize!
  manual_registrar.finalize!
  auto_registrar.finalize!

  @__finalized__ = true

  self.freeze if freeze
  self
end

.finalized?TrueClass, FalseClass

Return if a container was finalized

Returns:

  • (TrueClass, FalseClass)


261
262
263
# File 'lib/dry/system/container.rb', line 261

def finalized?
  @__finalized__.equal?(true)
end

.hooksObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



612
613
614
# File 'lib/dry/system/container.rb', line 612

def hooks
  @__hooks__ ||= Hash.new { |h, k| h[k] = [] }
end

.import(other) ⇒ Object

Registers another container for import

Examples:

# system/container.rb
class Core < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.auto_register = %w(lib/apis lib/core)
  end
end

# apps/my_app/system/container.rb
require 'system/container'

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname("/path/to/app")
    config.auto_register = %w(lib/apis lib/core)
  end

  import core: Core
end

Parameters:

  • other (Hash, Dry::Container::Namespace)


137
138
139
140
141
142
143
144
# File 'lib/dry/system/container.rb', line 137

def import(other)
  case other
  when Hash then importer.register(other)
  when Dry::Container::Namespace then super
  else
    raise ArgumentError, "+other+ must be a hash of names and systems, or a Dry::Container namespace"
  end
end

.importerObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



537
538
539
# File 'lib/dry/system/container.rb', line 537

def importer
  @importer ||= config.importer.new(self)
end

.inherited(klass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



617
618
619
620
621
622
623
624
625
626
627
# File 'lib/dry/system/container.rb', line 617

def inherited(klass)
  new_hooks = Container.hooks.dup

  hooks.each do |event, blocks|
    new_hooks[event].concat(blocks)
    new_hooks[event].concat(klass.hooks[event])
  end

  klass.instance_variable_set(:@__hooks__, new_hooks)
  super
end

.init(name) ⇒ self

Boots a specific component but calls only ‘init` lifecycle trigger

This way of booting is useful in places where a heavy dependency is needed but its started environment is not required

Examples:

MyApp.init(:persistence)

Parameters:

  • name (Symbol)

    The name of a registered bootable component

Returns:

  • (self)


340
341
342
343
# File 'lib/dry/system/container.rb', line 340

def init(name)
  booter.init(name)
  self
end

.injector(options = {}) ⇒ Object

Builds injector for this container

An injector is a useful mixin which injects dependencies into automatically defined constructor.

Examples:

# Define an injection mixin
#
# system/import.rb
Import = MyApp.injector

# Use it in your auto-registered classes
#
# lib/user_repo.rb
require 'import'

class UserRepo
  include Import['persistence.db']
end

MyApp['user_repo].db # instance under 'persistence.db' key

Parameters:

  • options (Hash) (defaults to: {})

    injector options



462
463
464
# File 'lib/dry/system/container.rb', line 462

def injector(options = {})
  Dry::AutoInject(self, options)
end

.load_component(key) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
# File 'lib/dry/system/container.rb', line 582

def load_component(key)
  return self if key?(key)

  component(key).tap do |component|
    if component.boot?
      booter.start(component)
    else
      root_key = component.root_key

      if (bootable_dep = component(root_key)).boot?
        booter.start(bootable_dep)
      elsif importer.key?(root_key)
        load_imported_component(component.namespaced(root_key))
      end

      if !key?(key)
        load_local_component(component)
      end
    end
  end

  self
end

.load_pathsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



512
513
514
# File 'lib/dry/system/container.rb', line 512

def load_paths
  @load_paths ||= []
end

.load_paths!(*dirs) ⇒ self

Sets load paths relative to the container’s root dir

Examples:

class MyApp < Dry::System::Container
  configure do |config|
    # ...
  end

  load_paths!('lib')
end

Parameters:

  • dirs (Array<String>)

Returns:

  • (self)


381
382
383
384
385
386
387
388
# File 'lib/dry/system/container.rb', line 381

def load_paths!(*dirs)
  dirs.map(&root.method(:join)).each do |path|
    next if load_paths.include?(path)
    load_paths << path
    $LOAD_PATH.unshift(path.to_s)
  end
  self
end

.load_registrations!(name) ⇒ Object



391
392
393
394
# File 'lib/dry/system/container.rb', line 391

def load_registrations!(name)
  manual_registrar.(name)
  self
end

.manual_registrarObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



532
533
534
# File 'lib/dry/system/container.rb', line 532

def manual_registrar
  @manual_registrar ||= config.manual_registrar.new(self)
end

.require_component(component) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



558
559
560
561
562
563
564
565
566
567
568
# File 'lib/dry/system/container.rb', line 558

def require_component(component)
  return if key?(component.identifier)

  unless component.file_exists?(load_paths)
    raise FileNotFoundError, component
  end

  require_path(component.path)

  yield
end

.require_from_root(*paths) ⇒ Object

Requires one or more files relative to the container’s root

Examples:

# single file
MyApp.require_from_root('lib/core')

# glob
MyApp.require_from_root('lib/**/*')

Parameters:

  • paths (Array<String>)

    one or more paths, supports globs too



478
479
480
481
482
483
484
# File 'lib/dry/system/container.rb', line 478

def require_from_root(*paths)
  paths.flat_map { |path|
    path.to_s.include?('*') ? Dir[root.join(path)] : root.join(path)
  }.each { |path|
    require path.to_s
  }
end

.require_path(path) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Allows subclasses to use a different strategy for required files.

E.g. apps that use ‘ActiveSupport::Dependencies::Loadable#require_dependency` will override this method to allow container managed dependencies to be reloaded for non-finalized containers.



577
578
579
# File 'lib/dry/system/container.rb', line 577

def require_path(path)
  require path
end

.resolve(key) ⇒ Object



505
506
507
508
509
# File 'lib/dry/system/container.rb', line 505

def resolve(key)
  load_component(key) unless finalized?

  super
end

.rootPathname

Returns container’s root path

Examples:

class MyApp < Dry::System::Container
  configure do |config|
    config.root = Pathname('/my/app')
  end
end

MyApp.root # returns '/my/app' pathname

Returns:

  • (Pathname)


500
501
502
# File 'lib/dry/system/container.rb', line 500

def root
  config.root
end

.shutdown!Object



360
361
362
363
# File 'lib/dry/system/container.rb', line 360

def shutdown!
  booter.shutdown
  self
end

.start(name) ⇒ self

Boots a specific component

As a result, ‘init` and `start` lifecycle triggers are called

Examples:

MyApp.start(:persistence)

Parameters:

  • name (Symbol)

    the name of a registered bootable component

Returns:

  • (self)


322
323
324
325
# File 'lib/dry/system/container.rb', line 322

def start(name)
  booter.start(name)
  self
end

.stop(name) ⇒ self

Stop a specific component but calls only ‘stop` lifecycle trigger

Examples:

MyApp.stop(:persistence)

Parameters:

  • name (Symbol)

    The name of a registered bootable component

Returns:

  • (self)


355
356
357
358
# File 'lib/dry/system/container.rb', line 355

def stop(name)
  booter.stop(name)
  self
end