Class: Dry::System::Container

Inherits:
Object
  • Object
show all
Extended by:
Configurable, Container::Mixin
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

Class Method Details

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



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

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.



452
453
454
# File 'lib/dry/system/container.rb', line 452

def auto_registrar
  @auto_registrar ||= config.auto_registrar.new(self)
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.



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

def booter
  @booter ||= config.booter.new(root.join("#{config.system_dir}/boot"))
end

.component(key, **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.



467
468
469
470
471
472
473
474
475
# File 'lib/dry/system/container.rb', line 467

def component(key, **options)
  Component.new(
    key,
    loader: config.loader,
    namespace: config.default_namespace,
    separator: config.namespace_separator,
    **options,
  )
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)


97
98
99
100
101
# File 'lib/dry/system/container.rb', line 97

def configure(&block)
  super(&block)
  load_paths!(config.system_dir)
  self
end

.enable_stubs!Object



15
16
17
# File 'lib/dry/system/stubs.rb', line 15

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

.finalize(name, &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.finalize(:db) do |container|
  require 'db'

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

# system/boot/db.rb
#
# Component registration with lifecycle triggers
MyApp.finalize(: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.finalize(: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:



211
212
213
214
# File 'lib/dry/system/container.rb', line 211

def finalize(name, &block)
  booter[name] = [self, block]
  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



245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/dry/system/container.rb', line 245

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

  yield(self) if block

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

  self.freeze if freeze
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)


129
130
131
132
133
134
135
136
# File 'lib/dry/system/container.rb', line 129

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.



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

def importer
  @importer ||= config.importer.new(self)
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)


289
290
291
292
# File 'lib/dry/system/container.rb', line 289

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



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

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.



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/dry/system/container.rb', line 491

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

  component(key).tap do |component|
    root_key = component.root_key

    if importer.key?(root_key)
      load_external_component(component.namespaced(root_key))
    else
      load_local_component(component)
    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.



442
443
444
# File 'lib/dry/system/container.rb', line 442

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)


311
312
313
314
315
316
317
318
# File 'lib/dry/system/container.rb', line 311

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



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

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.



457
458
459
# File 'lib/dry/system/container.rb', line 457

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

.require(*paths) ⇒ Object

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

Examples:

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

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

Parameters:

  • *paths (Array<String>)

    one or more paths, supports globs too



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

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



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

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

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

  Kernel.require(component.path)

  yield
end

.resolve(key) ⇒ Object



435
436
437
438
439
# File 'lib/dry/system/container.rb', line 435

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

  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)


430
431
432
# File 'lib/dry/system/container.rb', line 430

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)


270
271
272
273
# File 'lib/dry/system/container.rb', line 270

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