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.



570
571
572
# File 'lib/dry/system/container.rb', line 570

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 been 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:



409
410
411
412
# File 'lib/dry/system/container.rb', line 409

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.



504
505
506
# File 'lib/dry/system/container.rb', line 504

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:



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

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.



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

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.



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

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.



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

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.



494
495
496
# File 'lib/dry/system/container.rb', line 494

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.



519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/dry/system/container.rb', line 519

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,
      **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)


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

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



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

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
end

.finalized?TrueClass, FalseClass

Return if a container was finalized

Returns:

  • (TrueClass, FalseClass)


259
260
261
# File 'lib/dry/system/container.rb', line 259

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.



575
576
577
# File 'lib/dry/system/container.rb', line 575

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)


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

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.



514
515
516
# File 'lib/dry/system/container.rb', line 514

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.



580
581
582
583
584
585
586
587
588
589
590
# File 'lib/dry/system/container.rb', line 580

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)


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

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



439
440
441
# File 'lib/dry/system/container.rb', line 439

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.



547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/dry/system/container.rb', line 547

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



489
490
491
# File 'lib/dry/system/container.rb', line 489

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)


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

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



368
369
370
371
# File 'lib/dry/system/container.rb', line 368

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.



509
510
511
# File 'lib/dry/system/container.rb', line 509

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.



534
535
536
537
538
539
540
541
542
543
544
# File 'lib/dry/system/container.rb', line 534

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

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

  require component.path

  yield
end

.require_from_root(*paths) ⇒ Object

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

Examples:

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

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

Parameters:

  • *paths (Array<String>)

    one or more paths, supports globs too



455
456
457
458
459
460
461
# File 'lib/dry/system/container.rb', line 455

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

.resolve(key) ⇒ Object



482
483
484
485
486
# File 'lib/dry/system/container.rb', line 482

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)


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

def root
  config.root
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)


319
320
321
322
# File 'lib/dry/system/container.rb', line 319

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