diff --git a/lib/dry/system/container.rb b/lib/dry/system/container.rb index b324f8e4..dfb26232 100644 --- a/lib/dry/system/container.rb +++ b/lib/dry/system/container.rb @@ -65,6 +65,7 @@ class Container setting :auto_registrar, default: Dry::System::AutoRegistrar setting :manifest_registrar, default: Dry::System::ManifestRegistrar setting :provider_registrar, default: Dry::System::ProviderRegistrar + setting :provider_superclass, default: Dry::System::Provider::Source setting :importer, default: Dry::System::Importer # Expect "." as key namespace separator. This is not intended to be user-configurable. diff --git a/lib/dry/system/provider/source.rb b/lib/dry/system/provider/source.rb index 42ad58b1..796d3b33 100644 --- a/lib/dry/system/provider/source.rb +++ b/lib/dry/system/provider/source.rb @@ -37,10 +37,20 @@ class << self # @see Dry::System::Provider::SourceDSL # # @api private - def for(name:, group: nil, &block) - Class.new(self) { |klass| + def for(name:, group: nil, superclass: nil, &block) + superclass ||= self + + Class.new(superclass) { |klass| klass.source_name name klass.source_group group + + name_with_group = group ? "#{group}->#{name}" : name + klass.instance_eval <<~RUBY, __FILE__, __LINE__ + 1 + def name + "#{superclass.name}[#{name_with_group}]" + end + RUBY + SourceDSL.evaluate(klass, &block) if block } end @@ -58,14 +68,6 @@ def inherited(subclass) end end - # @api private - def name - source_str = source_name - source_str = "#{source_group}->#{source_str}" if source_group - - "Dry::System::Provider::Source[#{source_str}]" - end - # @api private def to_s "#<#{name}>" diff --git a/lib/dry/system/provider_registrar.rb b/lib/dry/system/provider_registrar.rb index 6f078735..0f139289 100644 --- a/lib/dry/system/provider_registrar.rb +++ b/lib/dry/system/provider_registrar.rb @@ -46,17 +46,25 @@ def freeze # @see Container.register_provider # @api private - def register_provider(name, from: nil, source: nil, if: true, **provider_options, &block) + def register_provider(name, from: nil, source: nil, superclass: nil, if: true, **provider_options, &block) raise ProviderAlreadyRegisteredError, name if providers.key?(name) if from && source.is_a?(Class) raise ArgumentError, "You must supply a block when using a provider source" end + if from && superclass + raise ArgumentError, "You may not supply a `superclass:` option with `from:`" + end + if block && source.is_a?(Class) raise ArgumentError, "You must supply only a `source:` option or a block, not both" end + if superclass && source.is_a?(Class) + raise ArgumentError, "You may not supply a `superclass:` option with a concrete `source:`" + end + return self unless binding.local_variable_get(:if) provider = @@ -72,6 +80,7 @@ def register_provider(name, from: nil, source: nil, if: true, **provider_options build_provider( name, source: source, + superclass: superclass, options: provider_options, &block ) @@ -195,8 +204,9 @@ def provider_paths } end - def build_provider(name, options:, source: nil, &block) - source_class = source || Provider::Source.for(name: name, &block) + def build_provider(name, options:, source: nil, superclass: nil, &block) + superclass ||= container.config.provider_superclass + source_class = source || Provider::Source.for(name: name, superclass: superclass, &block) Provider.new( **options, diff --git a/spec/integration/container/providers/custom_provider_superclass_spec.rb b/spec/integration/container/providers/custom_provider_superclass_spec.rb new file mode 100644 index 00000000..2fdc6671 --- /dev/null +++ b/spec/integration/container/providers/custom_provider_superclass_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +RSpec.describe "Providers / Custom provider superclass" do + let(:custom_superclass) do + module Test + class CustomSource < Dry::System::Provider::Source + def custom_api = :hello + end + end + + Test::CustomSource + end + + let(:source) do + module Test + class LoggerProvider < Dry::System::Provider::Source + def prepare + require "logger" + end + + def start + register(:logger, Logger.new(STDOUT)) + end + end + end + + Test::LoggerProvider + end + + subject(:system) do + module Test + class Container < Dry::System::Container + configure finalize_config: false do |config| + config.root = SPEC_ROOT.join("fixtures/app").realpath + end + end + end + + Test::Container + end + + before do + Dry::System.register_provider_source(:logger, group: :custom_group, source: source) + end + + describe "superclass override" do + subject(:provider_source) { system.providers[:test].source } + + specify "explicit superclass" do + system.register_provider(:test, superclass: custom_superclass) {} + + expect(provider_source.class).to be < custom_superclass + expect(provider_source.class.name).to eq "Test::CustomSource[test]" + expect(provider_source.custom_api).to eq :hello + end + + specify "configured superclass" do + system.config.provider_superclass = custom_superclass + + system.register_provider(:test) {} + + expect(provider_source.class).to be < custom_superclass + expect(provider_source.class.name).to eq "Test::CustomSource[test]" + expect(provider_source.custom_api).to eq :hello + end + end + + describe "invalid arguments" do + it "doesn't accept a superclass for a concrete source" do + source = Class.new(Dry::System::Provider::Source) + + expect { + system.register_provider(:invalid, source: source, superclass: custom_superclass) + }.to raise_error(ArgumentError, /superclass/) + end + + it "doesn't accept a superclass for an external provider" do + expect { + system.register_provider(:test, from: :custom_group, source: :logger, superclass: custom_superclass) + }.to raise_error(ArgumentError, /superclass/) + end + end +end