diff --git a/lib/dry/system/plugins.rb b/lib/dry/system/plugins.rb index 07824bc20..ccb5b0601 100644 --- a/lib/dry/system/plugins.rb +++ b/lib/dry/system/plugins.rb @@ -21,8 +21,8 @@ def initialize(name, mod, &block) end # @api private - def apply_to(system, options) - system.extend(stateful? ? mod.new(options) : mod) + def apply_to(system, **options) + system.extend(stateful? ? mod.new(**options) : mod) system.instance_eval(&block) if block system end @@ -90,13 +90,13 @@ def self.loaded_dependencies # @return [self] # # @api public - def use(name, options = {}) + def use(name, **options) return self if enabled_plugins.include?(name) raise PluginNotFoundError, name unless (plugin = Plugins.registry[name]) plugin.load_dependencies - plugin.apply_to(self, options) + plugin.apply_to(self, **options) enabled_plugins << name @@ -134,6 +134,9 @@ def enabled_plugins require "dry/system/plugins/zeitwerk" register(:zeitwerk, Plugins::Zeitwerk) + + require_relative "plugins/injector_mixin" + register(:injector_mixin, Plugins::InjectorMixin) end end end diff --git a/lib/dry/system/plugins/env.rb b/lib/dry/system/plugins/env.rb index 4aba4c038..e4d72788b 100644 --- a/lib/dry/system/plugins/env.rb +++ b/lib/dry/system/plugins/env.rb @@ -10,7 +10,7 @@ class Env < Module attr_reader :options # @api private - def initialize(options) + def initialize(**options) @options = options end diff --git a/lib/dry/system/plugins/injector_mixin.rb b/lib/dry/system/plugins/injector_mixin.rb new file mode 100644 index 000000000..da195a15c --- /dev/null +++ b/lib/dry/system/plugins/injector_mixin.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Dry + module System + module Plugins + # @api private + class InjectorMixin < Module + MODULE_SEPARATOR = "::" + + attr_reader :name + + def initialize(name: "Deps") + @name = name + end + + def extended(container) + container.after(:configure, &method(:define_mixin)) + end + + private + + def define_mixin(container) + inflector = container.config.inflector + + name_parts = name.split(MODULE_SEPARATOR) + + if name_parts[0] == "" + name_parts.delete_at(0) + root_module = Object + else + root_module = container_parent_module(container) + end + + mixin_parent_mod = define_parent_modules( + root_module, + name_parts, + inflector + ) + + mixin_parent_mod.const_set( + inflector.camelize(name_parts.last), + container.injector + ) + end + + def container_parent_module(container) + if container.name + parent_name = container.name.split(MODULE_SEPARATOR)[0..-2].join(MODULE_SEPARATOR) + container.config.inflector.constantize(parent_name) + else + Object + end + end + + def define_parent_modules(root_mod, name_parts, inflector) + return root_mod if name_parts.length == 1 + + name_parts[0..-2].reduce(root_mod) { |parent_mod, mod_name| + parent_mod.const_set(inflector.camelize(mod_name), Module.new) + } + end + end + end + end +end diff --git a/lib/dry/system/plugins/zeitwerk.rb b/lib/dry/system/plugins/zeitwerk.rb index 6dad8175c..80a687d6b 100644 --- a/lib/dry/system/plugins/zeitwerk.rb +++ b/lib/dry/system/plugins/zeitwerk.rb @@ -17,7 +17,7 @@ def self.dependencies attr_reader :options # @api private - def initialize(options) + def initialize(**options) @options = options super() end diff --git a/spec/integration/container/plugins/injector_mixin_spec.rb b/spec/integration/container/plugins/injector_mixin_spec.rb new file mode 100644 index 000000000..6f833713c --- /dev/null +++ b/spec/integration/container/plugins/injector_mixin_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe "Plugins / Injector mixin" do + describe "default options" do + it "creates a 'Deps' mixin in the container's parent module" do + module Test + class Container < Dry::System::Container + use :injector_mixin + configured! + end + end + + component = Object.new + Test::Container.register "component", component + + depending_obj = Class.new do + include Test::Deps["component"] + end.new + + expect(depending_obj.component).to be component + end + end + + describe "name given" do + it "creates a mixin with the given name in the container's parent module" do + module Test + class Container < Dry::System::Container + use :injector_mixin, name: "Inject" + configured! + end + end + + component = Object.new + Test::Container.register "component", component + + depending_obj = Class.new do + include Test::Inject["component"] + end.new + + expect(depending_obj.component).to be component + end + end + + describe "nested name given" do + it "creates a mixin with the given name in the container's parent module" do + module Test + class Container < Dry::System::Container + use :injector_mixin, name: "Inject::These::Pls" + configured! + end + end + + component = Object.new + Test::Container.register "component", component + + depending_obj = Class.new do + include Test::Inject::These::Pls["component"] + end.new + + expect(depending_obj.component).to be component + end + end + + describe "top-level name given" do + it "creates a mixin with the given name in the top-level module" do + module Test + class Container < Dry::System::Container + use :injector_mixin, name: "::Deps" + configured! + end + end + + component = Object.new + Test::Container.register "component", component + + depending_obj = Class.new do + include ::Deps["component"] + end.new + + expect(depending_obj.component).to be component + end + end +end