lib/dry/system/container.rb in dry-system-0.18.2 vs lib/dry/system/container.rb in dry-system-0.19.0
- old
+ new
@@ -9,19 +9,21 @@
require "dry/core/deprecations"
require "dry/system"
require "dry/system/errors"
-require "dry/system/loader"
require "dry/system/booter"
require "dry/system/auto_registrar"
require "dry/system/manual_registrar"
require "dry/system/importer"
require "dry/system/component"
require "dry/system/constants"
require "dry/system/plugins"
+require_relative "component_dir"
+require_relative "config/component_dirs"
+
module Dry
module System
# Abstract container class to inherit from
#
# Container class is treated as a global registry with all system components.
@@ -59,28 +61,26 @@
# # 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')
+ # add_dirs_to_load_paths!('lib')
# end
#
# @api public
class Container
extend Dry::Configurable
extend Dry::Container::Mixin
extend Dry::System::Plugins
setting :name
- setting :default_namespace
setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
setting :system_dir, "system"
setting :bootable_dirs, ["system/boot"]
setting :registrations_dir, "container"
- setting :auto_register, []
+ setting :component_dirs, Config::ComponentDirs.new, cloneable: true
setting :inflector, Dry::Inflector.new
- setting :loader, Dry::System::Loader
setting :booter, Dry::System::Booter
setting :auto_registrar, Dry::System::AutoRegistrar
setting :manual_registrar, Dry::System::ManualRegistrar
setting :importer, Dry::System::Importer
setting(:components, {}, reader: true, &:dup)
@@ -105,11 +105,10 @@
super(name, *args, &block)
# TODO: dry-configurable needs a public API for this
config._settings << _settings[name]
self
end
- ruby2_keywords(:setting) if respond_to?(:ruby2_keywords, true)
# Configures the container
#
# @example
# class MyApp < Dry::System::Container
@@ -124,11 +123,10 @@
#
# @api public
def configure(&block)
hooks[:before_configure].each { |hook| instance_eval(&hook) }
super(&block)
- load_paths!(config.system_dir)
hooks[:after_configure].each { |hook| instance_eval(&hook) }
self
end
# Registers another container for import
@@ -383,83 +381,39 @@
def shutdown!
booter.shutdown
self
end
- # Sets load paths relative to the container's root dir
+ # Adds the directories (relative to the container's root) to the Ruby load path
#
# @example
# class MyApp < Dry::System::Container
# configure do |config|
# # ...
# end
#
- # load_paths!('lib')
+ # add_to_load_path!('lib')
# end
#
# @param [Array<String>] dirs
#
# @return [self]
#
# @api public
- 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)
+ def add_to_load_path!(*dirs)
+ dirs.reverse.map(&root.method(:join)).each do |path|
+ $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
end
self
end
# @api public
def load_registrations!(name)
manual_registrar.(name)
self
end
- # 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
- #
- # @example
- # 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
- #
- # @param [String] dir The dir name relative to the root dir
- #
- # @yield AutoRegistrar::Configuration
- # @see AutoRegistrar::Configuration
- #
- # @return [self]
- #
- # @api public
- def auto_register!(dir, &block)
- auto_registrar.(dir, &block)
- self
- end
-
# Builds injector for this container
#
# An injector is a useful mixin which injects dependencies into
# automatically defined constructor.
#
@@ -524,12 +478,12 @@
def root
config.root
end
# @api public
- def resolve(key, &block)
- load_component(key, &block) unless finalized?
+ def resolve(key)
+ load_component(key) unless finalized?
super
end
alias_method :registered?, :key?
@@ -556,12 +510,12 @@
true
end
end
# @api private
- def load_paths
- @load_paths ||= []
+ def component_dirs
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
end
# @api private
def booter
@booter ||= config.booter.new(boot_paths)
@@ -594,71 +548,10 @@
def importer
@importer ||= config.importer.new(self)
end
# @api private
- 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
-
- # @api private
- def require_component(component)
- return if registered?(component.identifier)
-
- raise FileNotFoundError, component unless component.file_exists?(load_paths)
-
- require_path(component.path)
-
- yield
- end
-
- # 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.
- #
- # @api private
- def require_path(path)
- require path
- end
-
- # @api private
- def load_component(key, &block)
- return self if registered?(key)
-
- component(key).tap do |component|
- if component.bootable?
- booter.start(component)
- else
- root_key = component.root_key
-
- if (root_bootable = component(root_key)).bootable?
- booter.start(root_bootable)
- 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
-
- # @api private
def after(event, &block)
hooks[:"after_#{event}"] << block
end
def before(event, &block)
@@ -670,48 +563,89 @@
@hooks ||= Hash.new { |h, k| h[k] = [] }
end
# @api private
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])
+ klass.hooks[event].concat blocks.dup
end
- klass.instance_variable_set(:@hooks, new_hooks)
klass.instance_variable_set(:@__finalized__, false)
+
super
end
- private
+ protected
# @api private
- def load_local_component(component, default_namespace_fallback = false, &block)
- if booter.bootable?(component) || component.file_exists?(load_paths)
- booter.boot_dependency(component) unless finalized?
+ def load_component(key)
+ return self if registered?(key)
- require_component(component) do
- register(component.identifier) { component.instance }
- end
- elsif !default_namespace_fallback
- load_local_component(component.prepend(config.default_namespace), true, &block)
+ component = component(key)
+
+ if component.bootable?
+ booter.start(component)
+ return self
+ end
+
+ booter.boot_dependency(component)
+ return self if registered?(key)
+
+ if component.file_exists?
+ load_local_component(component)
elsif manual_registrar.file_exists?(component)
manual_registrar.(component)
- elsif block_given?
- yield
- else
- raise ComponentLoadError, component
+ elsif importer.key?(component.identifier.root_key)
+ load_imported_component(component.identifier)
end
+
+ self
end
- # @api private
- def load_imported_component(component)
- container = importer[component.namespace]
- container.load_component(component.identifier)
- importer.(component.namespace, container)
+ private
+
+ def load_local_component(component)
+ if component.auto_register?
+ register(component.identifier, memoize: component.memoize?) { component.instance }
+ end
end
+
+ def load_imported_component(identifier)
+ import_namespace = identifier.root_key
+
+ container = importer[import_namespace]
+
+ container.load_component(identifier.dequalified(import_namespace).key)
+
+ importer.(import_namespace, container)
+ end
+
+ def component(identifier)
+ if (bootable_component = booter.find_component(identifier))
+ return bootable_component
+ end
+
+ # Find the first matching component from within the configured component dirs.
+ # If no matching component is found, return a plain component instance with no
+ # associated file path. This fallback is important because the component may
+ # still be loadable via the manual registrar or an imported container.
+ component_dirs.detect { |dir|
+ if (component = dir.component_for_identifier(identifier))
+ break component
+ end
+ } || Component.new(identifier)
+ end
+ end
+
+ # Default hooks
+ after :configure do
+ # Add appropriately configured component dirs to the load path
+ #
+ # Do this in a single pass to preserve ordering (i.e. earliest dirs win)
+ paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
+ arr << dir.path if dir.add_to_load_path
+ }
+ add_to_load_path!(*paths)
end
end
end
end