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.



644
645
646
# File 'lib/dry/system/container.rb', line 644

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:



448
449
450
451
# File 'lib/dry/system/container.rb', line 448

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.



568
569
570
# File 'lib/dry/system/container.rb', line 568

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:



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/dry/system/container.rb', line 231

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

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

  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.



250
251
252
253
254
255
256
257
258
# File 'lib/dry/system/container.rb', line 250

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.



261
262
263
264
265
266
267
268
269
# File 'lib/dry/system/container.rb', line 261

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.



563
564
565
# File 'lib/dry/system/container.rb', line 563

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.



558
559
560
# File 'lib/dry/system/container.rb', line 558

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.



583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/dry/system/container.rb', line 583

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)


114
115
116
117
118
119
# File 'lib/dry/system/container.rb', line 114

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



28
29
30
31
32
# File 'lib/dry/system/stubs.rb', line 28

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



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/dry/system/container.rb', line 309

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)


276
277
278
# File 'lib/dry/system/container.rb', line 276

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.



649
650
651
# File 'lib/dry/system/container.rb', line 649

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)


147
148
149
150
151
152
153
154
155
156
# File 'lib/dry/system/container.rb', line 147

def import(other)
  case other
  when Hash then importer.register(other)
  when Dry::Container::Namespace then super
  else
    raise ArgumentError, <<-STR
      +other+ must be a hash of names and systems, or a Dry::Container namespace
    STR
  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.



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

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.



654
655
656
657
658
659
660
661
662
663
664
# File 'lib/dry/system/container.rb', line 654

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)


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

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

.injector(options = { strategies: strategies }) ⇒ 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: { strategies: strategies })

    injector options



478
479
480
# File 'lib/dry/system/container.rb', line 478

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

.key?(key) ⇒ Boolean

Check if identifier is registered. If not, try to load the component

Parameters:

  • key (String, Symbol)

    Identifier

Returns:

  • (Boolean)


543
544
545
546
547
548
549
550
# File 'lib/dry/system/container.rb', line 543

def key?(key)
  if finalized?
    registered?(key)
  else
    registered?(key) || resolve(key) { return false }
    true
  end
end

.load_component(key, &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.



621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
# File 'lib/dry/system/container.rb', line 621

def load_component(key, &block)
  return self if registered?(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

      load_local_component(component, &block) unless registered?(key)
    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.



553
554
555
# File 'lib/dry/system/container.rb', line 553

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)


396
397
398
399
400
401
402
403
404
# File 'lib/dry/system/container.rb', line 396

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



407
408
409
410
# File 'lib/dry/system/container.rb', line 407

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.



573
574
575
# File 'lib/dry/system/container.rb', line 573

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

.registered?(key) ⇒ Boolean

Whether a key is registered (doesn’t trigger loading)

Parameters:

  • key (String, Symbol)

    Identifier

Returns:

  • (Boolean)


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

alias_method :registered?, :key?

.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.

Raises:



599
600
601
602
603
604
605
606
607
# File 'lib/dry/system/container.rb', line 599

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

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

  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



494
495
496
497
498
499
500
# File 'lib/dry/system/container.rb', line 494

def require_from_root(*paths)
  paths.flat_map { |path|
    path.to_s.include?('*') ? ::Dir[root.join(path)].sort : 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.



616
617
618
# File 'lib/dry/system/container.rb', line 616

def require_path(path)
  require path
end

.resolve(key, &block) ⇒ Object



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

def resolve(key, &block)
  load_component(key, &block) 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)


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

def root
  config.root
end

.shutdown!Object



375
376
377
378
# File 'lib/dry/system/container.rb', line 375

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)


337
338
339
340
# File 'lib/dry/system/container.rb', line 337

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)


370
371
372
373
# File 'lib/dry/system/container.rb', line 370

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

.strategies(value = nil) ⇒ Object



90
91
92
93
94
95
96
# File 'lib/dry/system/container.rb', line 90

def strategies(value = nil)
  if value
    @strategies = value
  else
    @strategies ||= Dry::AutoInject::Strategies
  end
end