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